feat(proxy): opt-in same-target 429 wait-and-retry before key failover (#487) - #865
feat(proxy): opt-in same-target 429 wait-and-retry before key failover (#487)#865harryzhou2000 wants to merge 27 commits into
Conversation
lidge-jun#487) Codex never retries HTTP 429 (openai/codex#30471 keeps retry_429=false and the misleading 'exceeded retry limit' error), and single-key pools have no failover, so provider-level retryOn429 waits (Retry-After or fixed interval) and replays the identical pre-stream request on the same key before any key rotation. - types.ts: RateLimitRetryPolicy + OcxProviderConfig.retryOn429 - config.ts: lenient zod validation (strip unknown; typo degrades, never rejects config) - key-failover.ts: rateLimitRetryPolicyFor + rateLimitRetryDelayMs (Retry-After capped at maxIntervalMs) - responses/core.ts: recovery-loop replay before multi-key failover; abort-aware sleep; covers Responses, chat completions, and routed Claude messages - usage/log.ts: AttemptRecoveryKind 'rate-limit-429' - docs-site configuration reference + structure/04 transport note + devlog plan unit - tests: policy unit tests + e2e (single-key replay, passthrough without knob, exhaustion, retry-before-failover ordering) Verified: typecheck, privacy scan, 43/43 retry-related tests; full suite failures are pre-existing on pristine dev in this environment (WS/auth/connection-refused) plus missing-gui-deps artifacts that pass once installed.
|
Warning Review limit reached
Next review available in: 8 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds opt-in, same-key HTTP 429 retries for API-key providers. Retries use bounded delays, ChangesRate-limit retry policy
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant ResponsesCore
participant ProviderTransport
participant UpstreamProvider
Client->>ResponsesCore: Submit request
ResponsesCore->>ProviderTransport: Send request with current API key
ProviderTransport->>UpstreamProvider: Forward request
UpstreamProvider-->>ResponsesCore: Return HTTP 429
ResponsesCore->>ResponsesCore: Release body and wait
ResponsesCore->>ProviderTransport: Replay identical request
ProviderTransport->>UpstreamProvider: Forward replay on same key
UpstreamProvider-->>ResponsesCore: Return success or final 429
ResponsesCore->>ResponsesCore: Rotate key after retry budget exhaustion
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
- usage/log: whitelist the rate-limit-429 recovery kind so persisted usage rows and post-restart /api/logs keep the reason - key-failover: gate retryOn429 to key-auth providers (no same-token OAuth replays, no silent no-op on forward passthrough); accept Retry-After 0 as immediate - config: cap maxIntervalMs at the effective 10-minute cooldown ceiling - core: hoist the retry budget outside the recovery loop so 413/401 replays cannot re-arm it; cancel the unread 429 body before aborting on client cancel - gui: add rate-limit-429 (and pre-existing anthropic-oauth-429) to the Logs union - tests: OAuth/forward gating, HTTP-date Retry-After, Retry-After 0, persisted recovery-kind, and a deterministic direct-handler abort test (real-socket disconnect propagation is async in Bun, so the e2e version was timing-flaky) - docs: key-auth-only note, maxIntervalMs cap, abort-propagation nuance, locale rows
… xhigh) - core: the Responses passthrough wire (openai-responses key-auth gateways, e.g. the built-in DeepSeek preset) now replays 429 on the same key pre-relay, before the forward-pool logic; Anthropic terminal-guard continuations replay before key/account failover - images/loop + web-search/loop: same-target replay before on429 key rotation via a new retryOn429Policy dep (abort-aware, heartbeat seams preserved) - key-failover: the fixed fallback is now capped at maxIntervalMs (a single wait never exceeds the cap); authMode local runtimes are gated out alongside oauth/forward, so the knob matches the documented API-key scope exactly - tests: key-auth openai-responses passthrough e2e, terminal-guard continuation replay, image/web-search same-key replay (rotation stays zero), fallback cap, local-mode gating - docs: coverage and cap wording in devlog 010, structure/04, and all five configuration locale rows
There was a problem hiding this comment.
Actionable comments posted: 10
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@devlog/_plan/260802_429_same_target_retry/010_design.md`:
- Around line 57-67: Update the latency and concurrency sections of the retry
design to distinguish retry-wait time from total request latency, noting that
connection, response, and configured timeout durations also contribute. Revise
the request-volume bound to account for multi-key failover, including the
possibility of exhausting the retry budget on one key before attempting another,
and document the combined bound alongside the existing failover behavior.
- Around line 7-9: Update same-key retry handling around the
continuation-request rebuild in core response processing so every retry replays
a cached, body-safe representation of the identical upstream request, including
serialized body and authentication headers, rather than rebuilding it each time.
Apply this to passthrough Responses and Anthropic terminal continuations; only
rebuild after the target or adapter changes, defining deterministic equivalence
where adapters must rebuild. Extend the rate-limit retry and terminal-guard
tests to assert body and authentication/header equality, not just send counts.
- Around line 32-34: Update rate-limit retry authentication handling to fail
closed: normalize omitted key-provider auth modes to "key", preserve "local" in
providerConfigSeed (including the mapping in derive), and make
rateLimitRetryPolicyFor allow retries only when authMode === "key". Add
regression coverage for omitted, local, and unknown authentication modes while
preserving existing key-provider behavior.
In `@docs-site/src/content/docs/reference/configuration.md`:
- Line 353: Update the retryOn429 documentation across the English, Japanese,
Korean, Russian, and Chinese provider and adapter reference pages. State that it
applies only to authMode: "key" and excludes oauth, forward, and local
providers; document same-key replay, raw openai-responses passthrough,
translated openai-chat/Anthropic requests, and that custom runTurn transports
are excluded. Keep the existing defaults and behavior description consistent
across all pages.
In `@src/images/loop.ts`:
- Around line 467-490: The 429 retry waits must not consume the cumulative
header deadline in either loop. In src/images/loop.ts lines 467-490, update the
flow around the headerDeadline and rateLimitRetryPolicy retry loop so each
deliberate wait is excluded, while preserving the configured retry outcome and
preventing the 504 header-timeout path from firing due to that wait; apply the
identical change in src/web-search/loop.ts lines 361-384 around its
headerDeadline and retry loop.
In `@src/providers/key-failover.ts`:
- Around line 76-96: Ensure local providers cannot enter the key-failover retry
path when authMode is undefined: preserve authMode: "local" through the provider
derivation/routing flow or pass the registry auth kind into
rateLimitRetryPolicyFor, while retaining undefined as the default for custom
API-key providers. Add a regression test covering a local provider configured
with retryOn429: {}.
In `@src/server/responses/core.ts`:
- Around line 1631-1671: Update the retry closure in the passthrough 429 loop to
pass `"rate-limit-429"` to `noteAttemptSend` whenever `fetchWithTransientRetry`
provides no recovery kind, while preserving any recovery kind it does provide.
Keep the existing `rateLimitPolicy` retry flow unchanged.
- Around line 2629-2665: Hoist the rateLimitPolicy and rateLimitRetries
declarations out of the terminal continuation while loop, placing them beside
imageTierBias so the retry budget persists across key rotation, account
rotation, and image-tier 413 continues. Resolve rateLimitPolicy once from the
initial route.provider and preserve the existing retry loop behavior while
preventing the budget from resetting per iteration.
In `@tests/rate-limit-retry.test.ts`:
- Around line 99-159: The retry tests lack coverage proving the retry budget is
scoped per request rather than reset during failover. Add focused regression
coverage near the existing retry-loop tests using a two-key provider pool,
retryOn429 with attempts set to 1, and upstream responses that always return
429; assert the total upstream send count remains within the request-wide bound,
covering both the main recovery loop and terminal-continuation path.
In `@tests/usage-log.test.ts`:
- Around line 39-62: Replace the as never cast in the
persists-the-rate-limit-429-recovery-kind-on-attempts test with a
PersistedUsageEntry-typed object. Keep the recoveryKinds literal type-checked
against the recovery-kind union, and cast only individual fields that genuinely
require it while preserving the existing round-trip assertion.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: a0cd7539-4937-4e9d-bf2a-e78d059e114a
📒 Files selected for processing (22)
devlog/_plan/260802_429_same_target_retry/000_research.mddevlog/_plan/260802_429_same_target_retry/010_design.mddocs-site/src/content/docs/ja/reference/configuration.mddocs-site/src/content/docs/ko/reference/configuration.mddocs-site/src/content/docs/reference/configuration.mddocs-site/src/content/docs/ru/reference/configuration.mddocs-site/src/content/docs/zh-cn/reference/configuration.mdgui/src/pages/Logs.tsxsrc/config.tssrc/images/loop.tssrc/providers/key-failover.tssrc/server/responses/core.tssrc/types.tssrc/usage/log.tssrc/web-search/loop.tsstructure/04_transports-and-sidecars.mdtests/images/loop.test.tstests/rate-limit-retry.test.tstests/server-rate-limit-retry-e2e.test.tstests/terminal-guard-server.test.tstests/usage-log.test.tstests/web-search.test.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a06a416024
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… deadlines, recovery labels) - key-failover: fail closed - only authMode key (or the documented omitted default) may replay; unknown modes rejected; an already-expired HTTP-date Retry-After retries immediately (like Retry-After: 0) - derive: providerConfigSeed preserves the registry auth kind (incl. local) so the gate survives the seed round-trip and routing - core: release the unread 429 body BEFORE every backoff (main loop, passthrough, continuations); passthrough and continuation replay sends record the rate-limit-429 recovery kind; continuation budget hoisted outside the failover loop so rotation/413 cannot re-arm it; continuation waits sleep on the upstream signal so an SSE body-cancel aborts them too - images/web-search loops: release the 429 body before the wait, restart the response-header deadline after each deliberate wait (backoffs never consume the connect budget or surface as 504), and record rate-limit-429 on replay sends - config: load-time degradation for invalid optional retryOn429 fields (warn + drop the field) instead of tripping the whole schema and hiding all providers behind a default config; the management write boundary still rejects - tests: fail-closed auth modes (incl. unknown), expired HTTP-date, per-request budget across 2-key pools (e2e + terminal continuation), byte-identical replay bodies/auth (passthrough + continuation), recovery telemetry in both loops, config load degradation, registry auth-kind preservation, typed usage-log entry - docs: retry-wait vs total-latency and attempts+poolKeys volume bounds, identical-replay equivalence, deadline/backoff behavior in devlog 010 and structure/04; retryOn429 boundary notes in the provider guide and adapter reference (English + ja/ko/ru/zh-cn)
|
All 17 inline review comments are addressed in 58c36c9 (plus docstrings in 0feede1 for the coverage check). Highlights: fail-closed auth (registry now preserves authMode "local"), expired Retry-After dates retry immediately, unread 429 bodies are released before every backoff, rate-limit-429 is recorded on every retry surface, bridge loops restart their header deadline after each wait, the continuation budget is per-request, invalid optional retryOn429 fields degrade at load instead of discarding the config, and replay identity is asserted byte-for-byte in tests. Replies were posted on each thread; the 7 open threads are resolved. The PR remains a draft. |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/server/responses/core.ts (1)
2647-2685: 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy liftEmit upstream heartbeats during 429 backoff
The bridge checks upstream activity every 2,000 ms. It increments
stallTickswhen no adapter event arrives and aborts atceil(stallTimeoutSec * 1000 / 2000)ticks. Wire heartbeats do not reset this counter.The retry waits can be silent for up to 600,000 ms. This exceeds the default 300-second core budget and the default 330-second web-search budget.
Apply the fix to these live generators:
src/server/responses/core.ts:2647-2685src/web-search/loop.ts:363-393src/images/loop.ts:469-498Split each
sleepWithAbortcall into chunks shorter than 2,000 ms, such as 1,000 ms. Yield{ type: "heartbeat" }after every chunk, including the final chunk. Add regression coverage for a backoff longer thanstallTimeoutSec. The pre-stream retry loops insrc/server/responses/core.tsdo not require this change.Update the web-search timeout documentation and tests to state that retry backoff remains live through these upstream heartbeats.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/responses/core.ts` around lines 2647 - 2685, Update the live retry-backoff loops in src/server/responses/core.ts:2647-2685, src/web-search/loop.ts:363-393, and src/images/loop.ts:469-498 to split sleepWithAbort waits into sub-2,000 ms chunks, yielding a heartbeat after every chunk including the final one, while preserving abort handling; leave pre-stream retry loops unchanged. Add regression coverage for backoffs exceeding stallTimeoutSec, and update web-search timeout documentation and tests to describe continued liveness through upstream heartbeats.src/images/loop.ts (1)
469-476: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winKeep the retry budget scoped consistently.
src/images/loop.tsinitializesrateLimitRetriesinsideprepareIterationEvents, while onerunWithImageBridgerequest can execute multiple image-loop iterations. This can re-arm the same-key budget and exceed the documented per-request send bound.
src/images/loop.ts#L469-L476: move the policy and counter to request scope, or explicitly define the budget per iteration.structure/04_transports-and-sidecars.md#L235-L235: update theattempts + poolKeysbound if per-iteration scope is intentional.devlog/_plan/260802_429_same_target_retry/010_design.md#L95-L110: add a multi-iteration image-bridge regression test.#!/usr/bin/env bash set -euo pipefail rg -n -C 8 'prepareIterationEvents|rateLimitRetries|rateLimitRetryPolicy|HARD_CAP|retryOn429Policy' \ src/images/loop.ts tests/images/loop.test.tsAs per path instructions, add a focused regression in the flat Bun tests for this
src/**behavior change.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/images/loop.ts` around lines 469 - 476, Keep the 429 retry policy and counter request-scoped in prepareIterationEvents/runWithImageBridge so multiple image-loop iterations cannot reset the same-key retry budget; preserve the documented per-request send bound. Update structure/04_transports-and-sidecars.md:235 to reflect the chosen request-scoped bound, and extend devlog/_plan/260802_429_same_target_retry/010_design.md:95-110 with a multi-iteration image-bridge regression scenario. Add a focused regression to the flat Bun tests covering this behavior.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@devlog/_plan/260802_429_same_target_retry/010_design.md`:
- Around line 88-89: Align the design and associated tests with the current
implementation: update the expired HTTP-date and numeric Retry-After: 0 cases to
expect the configured fallback interval when the parser returns undefined,
unless the parser and rateLimitRetryDelayMs are intentionally changed to
propagate a zero delay. Keep the documented behavior, parser expectations, and
tests consistent.
In `@docs-site/src/content/docs/ja/reference/adapters.md`:
- Around line 41-43: Update the retry descriptions in
docs-site/src/content/docs/ja/reference/adapters.md:41-43,
docs-site/src/content/docs/ko/reference/adapters.md:48-50,
docs-site/src/content/docs/ru/reference/adapters.md:51-53, and
docs-site/src/content/docs/zh-cn/reference/adapters.md:46-48 to state that
same-key 429 replay occurs before other handling or failover, while preserving
the existing translated behavior and custom runTurn exception.
In `@docs-site/src/content/docs/zh-cn/guides/providers.md`:
- Around line 37-40: Update the Chinese provider guide passage describing
retryOn429 to explicitly state that it is opt-in and disabled by default unless
configured, while preserving the existing API-key-only restriction and OAuth,
forward, and local exclusions. Keep the wording consistent with the
corresponding English provider guide.
In `@src/config.ts`:
- Around line 1135-1149: Update the retryOn429 sanitization around the fields
definition and loop to iterate over all keys in policy, warning and ignoring any
key not in the recognized field set. Preserve the existing validators and
warning behavior for known fields with invalid values, and continue rebuilding
p.retryOn429 from only accepted entries.
In `@src/images/loop.ts`:
- Around line 479-486: Await the unread 429 response body cancellation in the
retry flow around sleepWithAbort, while preserving handling for already-closed
bodies and cancellation failures. Update
devlog/_plan/260802_429_same_target_retry/010_design.md lines 53-55 and
structure/04_transports-and-sidecars.md lines 236-237 only as needed to keep
their “release before backoff” claims accurate after the implementation change.
- Around line 482-498: The retry flow around sleepWithAbort in
src/images/loop.ts lines 482-498 must clear headerDeadline before sleeping,
check signal.aborted immediately afterward and throw the existing 499 LoopError
before telemetry or replay, then create the replacement deadline before
onRateLimitRetrySend and fetchOnce. Update
structure/04_transports-and-sidecars.md lines 238-242 to preserve and document
the 499-before-replay guarantee; no other behavior needs changing.
---
Outside diff comments:
In `@src/images/loop.ts`:
- Around line 469-476: Keep the 429 retry policy and counter request-scoped in
prepareIterationEvents/runWithImageBridge so multiple image-loop iterations
cannot reset the same-key retry budget; preserve the documented per-request send
bound. Update structure/04_transports-and-sidecars.md:235 to reflect the chosen
request-scoped bound, and extend
devlog/_plan/260802_429_same_target_retry/010_design.md:95-110 with a
multi-iteration image-bridge regression scenario. Add a focused regression to
the flat Bun tests covering this behavior.
In `@src/server/responses/core.ts`:
- Around line 2647-2685: Update the live retry-backoff loops in
src/server/responses/core.ts:2647-2685, src/web-search/loop.ts:363-393, and
src/images/loop.ts:469-498 to split sleepWithAbort waits into sub-2,000 ms
chunks, yielding a heartbeat after every chunk including the final one, while
preserving abort handling; leave pre-stream retry loops unchanged. Add
regression coverage for backoffs exceeding stallTimeoutSec, and update
web-search timeout documentation and tests to describe continued liveness
through upstream heartbeats.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 9881d8f6-9808-4382-a953-a3a067cfc38b
📒 Files selected for processing (26)
devlog/_plan/260802_429_same_target_retry/010_design.mddocs-site/src/content/docs/guides/providers.mddocs-site/src/content/docs/ja/guides/providers.mddocs-site/src/content/docs/ja/reference/adapters.mddocs-site/src/content/docs/ko/guides/providers.mddocs-site/src/content/docs/ko/reference/adapters.mddocs-site/src/content/docs/reference/adapters.mddocs-site/src/content/docs/ru/guides/providers.mddocs-site/src/content/docs/ru/reference/adapters.mddocs-site/src/content/docs/zh-cn/guides/providers.mddocs-site/src/content/docs/zh-cn/reference/adapters.mdsrc/config.tssrc/images/loop.tssrc/providers/derive.tssrc/providers/key-failover.tssrc/server/responses/core.tssrc/web-search/loop.tsstructure/04_transports-and-sidecars.mdtests/config-user-edits.test.tstests/images/loop.test.tstests/provider-registry-parity.test.tstests/rate-limit-retry.test.tstests/server-rate-limit-retry-e2e.test.tstests/terminal-guard-server.test.tstests/usage-log.test.tstests/web-search.test.ts
…ne race, config warnings, locale docs) - images/web-search loops: AWAIT the unread 429 body cancellation before the backoff; clear the old header deadline BEFORE the sleep; re-check client cancellation after the wait so 499 wins over stale-deadline edges; start the fresh deadline before telemetry and replay - config: sanitizeRetryOn429ForLoad warns about misnamed keys (e.g. attempt) instead of silently dropping them - docs: locale adapter pages state same-key replay runs before other handling/failover; provider guide (English + ja/ko/ru/zh-cn) states retryOn429 is opt-in (absent = off); devlog and structure/04 note the awaited cancellation and the 499-before-replay guarantee - test: config-user-edits covers the misnamed-key drop
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
devlog/_plan/260802_429_same_target_retry/010_design.md (1)
47-53: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDocument the
runTurnexception for bridge retries.
src/images/loop.tsenters theadapter.runTurnbranch at Lines 355-433 and returns before the HTTP 429 retry loop at Lines 477-513. A key-auth customrunTurnadapter therefore does not receive this HTTP retry policy. State that bridge retries apply to HTTP adapters only and exclude customrunTurntransports.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@devlog/_plan/260802_429_same_target_retry/010_design.md` around lines 47 - 53, Update the retry-policy documentation near the bridge retry references to state that image/video bridge retries apply only to HTTP adapters. Explicitly exclude custom adapters using the adapter.runTurn path in src/images/loop.ts, which returns before the HTTP 429 retry loop; do not imply that runTurn transports receive the same wait-and-replay behavior.src/server/responses/core.ts (1)
2668-2707: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winAwait terminal-continuation body cancellation before the backoff.
At Line 2679,
response.body.cancel()is detached withvoid.sleepWithAbort()then starts at Line 2681 while cancellation can still be pending. Under repeated 429 responses, unread bodies can remain active through the retry wait and replay.Await cancellation before sleeping.
Proposed fix
- try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ } + try { await response.body?.cancel().catch(() => {}); } catch { /* already closed */ }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/responses/core.ts` around lines 2668 - 2707, In the terminal-continuation retry loop, update the response body cleanup before sleepWithAbort to await response.body cancellation rather than detaching the promise. Preserve the existing safe handling for absent or already-closed bodies, and ensure the backoff starts only after cancellation has settled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@devlog/_plan/260802_429_same_target_retry/010_design.md`:
- Around line 47-53: Update the retry-policy documentation near the bridge retry
references to state that image/video bridge retries apply only to HTTP adapters.
Explicitly exclude custom adapters using the adapter.runTurn path in
src/images/loop.ts, which returns before the HTTP 429 retry loop; do not imply
that runTurn transports receive the same wait-and-replay behavior.
In `@src/server/responses/core.ts`:
- Around line 2668-2707: In the terminal-continuation retry loop, update the
response body cleanup before sleepWithAbort to await response.body cancellation
rather than detaching the promise. Preserve the existing safe handling for
absent or already-closed bodies, and ensure the backoff starts only after
cancellation has settled.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 72915daa-eace-482c-83cd-3c1d098cc169
📒 Files selected for processing (24)
devlog/_plan/260802_429_same_target_retry/010_design.mddocs-site/src/content/docs/guides/providers.mddocs-site/src/content/docs/ja/guides/providers.mddocs-site/src/content/docs/ja/reference/adapters.mddocs-site/src/content/docs/ko/guides/providers.mddocs-site/src/content/docs/ko/reference/adapters.mddocs-site/src/content/docs/ru/guides/providers.mddocs-site/src/content/docs/ru/reference/adapters.mddocs-site/src/content/docs/zh-cn/guides/providers.mddocs-site/src/content/docs/zh-cn/reference/adapters.mdsrc/config.tssrc/images/loop.tssrc/providers/derive.tssrc/providers/key-failover.tssrc/server/responses/core.tssrc/types.tssrc/web-search/loop.tsstructure/04_transports-and-sidecars.mdtests/config-user-edits.test.tstests/images/loop.test.tstests/server-rate-limit-retry-e2e.test.tstests/terminal-guard-server.test.tstests/usage-log.test.tstests/web-search.test.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1af83c39cb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… send telemetry, invalid master switch, secret-safe warnings)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d4e1978821
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ncel, post-sleep abort re-checks, pinned xAI req-id)
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/server/responses/core.ts (1)
2617-2666: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReduce the duplicated recovery-label expression in
fetchContinuation.Lines 2638 and 2647 both derive the same label from
replay. The second one additionally merges the transient-retry kind. The logic is correct, but the label rule now lives in two places, so a future change to the label (for example a distinct kind for continuation replays) can easily update only one branch.♻️ Optional consolidation
if (continuationEstimate !== undefined) logCtx.usageLogInputTokens = continuationEstimate; + const replayKind: AttemptRecoveryKind | undefined = replay ? "rate-limit-429" : undefined; try { if (activeAdapter.fetchResponse) { - noteAttemptSend(logCtx.activeAttempt, continuationEstimate, replay ? "rate-limit-429" : undefined); + noteAttemptSend(logCtx.activeAttempt, continuationEstimate, replayKind); return await activeAdapter.fetchResponse(continuationRequest, { @@ return await fetchWithResetRetry( recovery => { - noteAttemptSend(logCtx.activeAttempt, continuationEstimate, recovery ?? (replay ? "rate-limit-429" : undefined)); + noteAttemptSend(logCtx.activeAttempt, continuationEstimate, recovery ?? replayKind);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/responses/core.ts` around lines 2617 - 2666, Consolidate the replay-derived recovery label in fetchContinuation by computing it once before the fetch branches, then reuse that value in both noteAttemptSend calls while preserving the fetchWithResetRetry recovery override behavior.src/config.ts (1)
1144-1158: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueInclude the accepted range in the warning for out-of-range numbers.
The validators at Lines 1146-1148 reject both wrong types and out-of-range numbers, but the warning at Line 1157 reports only
typeof value. A user who writes"attempts": 999seesproviders.x.retryOn429.attempts (number) is invalid, which gives no hint about the accepted bound. Numbers and booleans cannot carry secret material, so the range can be stated safely while the value itself stays unlogged.♻️ Proposed diagnostic improvement
- const fields: Array<[string, (value: unknown) => boolean]> = [ - ["enabled", value => typeof value === "boolean"], - ["attempts", value => typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 20], - ["intervalMs", value => typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 600_000], - ["maxIntervalMs", value => typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 600_000], - ["respectRetryAfter", value => typeof value === "boolean"], + const fields: Array<[string, (value: unknown) => boolean, string]> = [ + ["enabled", value => typeof value === "boolean", "expected boolean"], + ["attempts", value => typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 20, "expected integer 1-20"], + ["intervalMs", value => typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 600_000, "expected integer 100-600000"], + ["maxIntervalMs", value => typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 600_000, "expected integer 100-600000"], + ["respectRetryAfter", value => typeof value === "boolean", "expected boolean"], ]; const cleaned: Record<string, unknown> = {}; - for (const [key, isValid] of fields) { + for (const [key, isValid, expectation] of fields) { const value = policyRecord[key]; if (value === undefined) continue; if (isValid(value)) cleaned[key] = value; // Log only the received type, never the value (provider config can hold secrets). - else console.warn(`⚠️ config.json providers.${name}.retryOn429.${key} (${typeof value}) is invalid — ignoring the field`); + else console.warn(`⚠️ config.json providers.${name}.retryOn429.${key} (${typeof value}) is invalid — ${expectation}; ignoring the field`); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/config.ts` around lines 1144 - 1158, Update the validation warning in the retry policy field loop around the `fields` validators to include the accepted ranges for numeric keys such as `attempts`, `intervalMs`, and `maxIntervalMs`, while retaining type-only reporting and never logging the received value. Keep boolean warnings unchanged and preserve the existing validation and field-cleaning behavior.src/images/loop.ts (1)
483-515: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winBound pre-header 429 retries before creating SSE
For non-
runTurnimage requests and all web-search requests,prepareIterationDrainedconsumes the retry heartbeat beforebridgeToResponsesSSEis created. The client receives no response headers while same-target 429 retries wait. The default policy can wait 3 minutes; valid configuration can wait up to 200 minutes (20 × 600_000ms). Bound the aggregate eager-phase wait to the request header budget, or move these retries into the liveproduce()phase. Apply the fix to both loops.runTurnimage requests already skip the eager drain.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/images/loop.ts` around lines 483 - 515, Bound same-target 429 retry waiting during the eager preparation phase so it cannot exceed the request header-timeout budget before SSE creation, or defer the retries into the live produce phase. Apply the corresponding fix to the retry loop in src/images/loop.ts lines 483-515 and src/web-search/loop.ts lines 380-412; preserve the existing runTurn image behavior, which already skips eager draining.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/providers/xai-transport.ts`:
- Around line 135-138: Rename the second parameter of withGeneratedRequestId to
reflect that it receives the already-resolved request ID, whether configured or
generated. Update all references within the helper while preserving the single
fallback in the request setup where configuredRequestId ?? randomUUID() is
computed, ensuring retries continue using the same ID.
In `@tests/config-user-edits.test.ts`:
- Around line 183-185: In the test assertion for the retryOn429 diagnostic,
replace the loose "string" substring check with an assertion anchored to the
retryOn429 field and its parenthesized received type. Keep the secret-redaction
assertion unchanged and ensure the expectation specifically verifies the
type-only warning from loadConfig().
---
Outside diff comments:
In `@src/config.ts`:
- Around line 1144-1158: Update the validation warning in the retry policy field
loop around the `fields` validators to include the accepted ranges for numeric
keys such as `attempts`, `intervalMs`, and `maxIntervalMs`, while retaining
type-only reporting and never logging the received value. Keep boolean warnings
unchanged and preserve the existing validation and field-cleaning behavior.
In `@src/images/loop.ts`:
- Around line 483-515: Bound same-target 429 retry waiting during the eager
preparation phase so it cannot exceed the request header-timeout budget before
SSE creation, or defer the retries into the live produce phase. Apply the
corresponding fix to the retry loop in src/images/loop.ts lines 483-515 and
src/web-search/loop.ts lines 380-412; preserve the existing runTurn image
behavior, which already skips eager draining.
In `@src/server/responses/core.ts`:
- Around line 2617-2666: Consolidate the replay-derived recovery label in
fetchContinuation by computing it once before the fetch branches, then reuse
that value in both noteAttemptSend calls while preserving the
fetchWithResetRetry recovery override behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3f71039b-4261-460f-853e-cf5dc91ff470
📒 Files selected for processing (11)
src/config.tssrc/images/loop.tssrc/providers/xai-transport.tssrc/server/responses/core.tssrc/web-search/loop.tstests/config-user-edits.test.tstests/images/loop.test.tstests/rate-limit-retry.test.tstests/terminal-guard-server.test.tstests/web-search.test.tstests/xai-transport.test.ts
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
docs-site/src/content/docs/ja/guides/providers.md (1)
146-146: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winFix the Markdown table separator.
Line 146 uses
--- | --- |without a leading pipe.markdownlint-cli2reports MD055 for this line. Change it to| --- | --- |.Proposed fix
- --- | --- | +| --- | --- |🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs-site/src/content/docs/ja/guides/providers.md` at line 146, Update the Markdown table separator at the affected location from “--- | --- |” to “| --- | --- |” so it has the required leading pipe and satisfies markdownlint MD055.Source: Linters/SAST tools
docs-site/src/content/docs/guides/providers.md (1)
347-354: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winSynchronize the locale guides with the Copilot wire rules.
Add the Copilot routing paragraph before the Cursor section in
ja(line 268),ko(line 269),ru(line 281), andzh-cn(line 249). Include the GPT-5 model list,openai-responsesrouting,openai-chatbehavior for other Copilot models,modelAdaptersprecedence, and thegpt-5.4-nanooverride example.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs-site/src/content/docs/guides/providers.md` around lines 347 - 354, Synchronize the localized provider guides by adding the Copilot routing paragraph before the Cursor section in the ja, ko, ru, and zh-cn guides. Preserve the complete behavior described in the source paragraph: list all specified GPT-5 models, route them through openai-responses by default, keep other Copilot models on openai-chat, document modelAdapters precedence, and include the gpt-5.4-nano override example.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@gui/src/pages/Logs.tsx`:
- Around line 84-85: Localize all recovery reasons rendered by LogDetailDialog
instead of displaying AttemptRecoveryKind values directly. Add an i18n mapping
covering every AttemptRecoveryKind, including rate-limit-429 and
anthropic-oauth-429, and render the translated labels for attempt.recoveryKinds.
Add corresponding translation keys for both new reasons in every GUI locale.
In `@src/config.ts`:
- Around line 566-574: Extract the duplicated retryOn429 bounds into a shared
schema near the config definitions, and derive load-time sanitizer checks from
that schema while preserving lenient handling that does not discard providers.
Update providerManagementConfigError() to validate retryOn429, including
rejecting invalid values and unknown keys, before /api/providers POST persists
the candidate; reuse the shared validator rather than adding separate bounds.
In `@src/server/responses/core.ts`:
- Around line 2505-2528: Introduce a single helper near the retry/cache state
that performs transport-token invalidation together with the associated
mutation, then replace the standalone transportToken increments at the OAuth
refresh, key rotation, Anthropic account rotation, image-tier bias, and
continuation paths. Update each mutation site to use this helper so changes to
parsed, route.provider, activeAdapter, or related request-shaping state cannot
bypass cache invalidation.
- Around line 2424-2468: After the buildRequest try/catch, capture the
successfully initialized request in a const AdapterRequest named builtRequest.
Replace subsequent initialRequest references in fetchWithResetRetry,
fetchWithHeaderTimeout, and related cleanup paths with builtRequest so the retry
callback satisfies strict TypeScript narrowing while preserving existing
behavior.
---
Outside diff comments:
In `@docs-site/src/content/docs/guides/providers.md`:
- Around line 347-354: Synchronize the localized provider guides by adding the
Copilot routing paragraph before the Cursor section in the ja, ko, ru, and zh-cn
guides. Preserve the complete behavior described in the source paragraph: list
all specified GPT-5 models, route them through openai-responses by default, keep
other Copilot models on openai-chat, document modelAdapters precedence, and
include the gpt-5.4-nano override example.
In `@docs-site/src/content/docs/ja/guides/providers.md`:
- Line 146: Update the Markdown table separator at the affected location from
“--- | --- |” to “| --- | --- |” so it has the required leading pipe and
satisfies markdownlint MD055.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: d5e15489-650b-49a3-b594-8a71430bb4fa
📒 Files selected for processing (22)
docs-site/src/content/docs/guides/providers.mddocs-site/src/content/docs/ja/guides/providers.mddocs-site/src/content/docs/ja/reference/adapters.mddocs-site/src/content/docs/ja/reference/configuration/providers.mddocs-site/src/content/docs/ko/guides/providers.mddocs-site/src/content/docs/ko/reference/adapters.mddocs-site/src/content/docs/ko/reference/configuration/providers.mddocs-site/src/content/docs/reference/adapters.mddocs-site/src/content/docs/reference/configuration/providers.mddocs-site/src/content/docs/ru/guides/providers.mddocs-site/src/content/docs/ru/reference/adapters.mddocs-site/src/content/docs/ru/reference/configuration/providers.mddocs-site/src/content/docs/zh-cn/guides/providers.mddocs-site/src/content/docs/zh-cn/reference/adapters.mddocs-site/src/content/docs/zh-cn/reference/configuration/providers.mdgui/src/pages/Logs.tsxsrc/config.tssrc/providers/derive.tssrc/server/responses/core.tssrc/types.tssrc/usage/log.tsstructure/04_transports-and-sidecars.md
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Current head and reported merge commit ==='
git rev-parse --short HEAD
git show --no-patch --format='commit=%H%nparents=%P%nsubject=%s' 528b4550
printf '%s\n' '=== File structure ==='
ast-grep outline src/server/responses/core.ts --items all --type function --match 'applyServiceTierGate|handleResponses' --view expanded
printf '%s\n' '=== Relevant imports ==='
rg -n -C 4 'providerModelWebsocketUpstreamStreaming|AdapterRequest|rateLimitRetryDelayMs|rateLimitRetryPolicyFor' src/server/responses/core.ts
printf '%s\n' '=== Service-tier and handler documentation block ==='
rg -n -C 12 'function applyServiceTierGate|async function handleResponses|function handleResponses|applyServiceTierGate' src/server/responses/core.ts
printf '%s\n' '=== Merge resolution diff ==='
git show --format= --unified=20 528b4550 -- src/server/responses/core.tsLength of output: 22815
The inspected merge diff shows no regression in either resolved hunk. I will resume normal review. ✅ Action performedReviews resumed. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs-site/src/content/docs/ja/guides/providers.md (1)
145-151: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winRestore the leading pipe on the delimiter row of the API key catalog table.
Line 146 is
--- | --- |. The leading|is missing, while the header row at Line 145 has it. markdownlint flags this as MD055 (table-pipe-style). remark/CommonMark can then fail to recognize the delimiter row, so the whole API-key catalog table risks rendering as literal text instead of a table. The English source atdocs-site/src/content/docs/guides/providers.md:218uses| --- | --- |.Note: the same malformed shape already exists at unchanged Lines 246 and 301 of this file. Fixing those too keeps the page consistent, but only Line 146 is inside this change.
📝 Proposed fix for the delimiter row
| プロバイダー | ベース URL | - --- | --- | +| --- | --- | | **OpenAI (API キー)** | `https://api.openai.com/v1` |🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs-site/src/content/docs/ja/guides/providers.md` around lines 145 - 151, Restore the leading pipe on the delimiter row immediately beneath the API-key catalog table header so it matches the header and English source format, using a complete `| --- | --- |` row. Also correct the same malformed delimiter rows at the unchanged table sections around the later provider tables to keep the page consistent.Sources: Path instructions, Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/server/responses/core.ts`:
- Around line 2459-2468: Capture initialRequest in a const after the try/catch
and use that const throughout the fetchWithResetRetry callback at
src/server/responses/core.ts lines 2459-2468. Similarly, capture
continuationRequest in a const after the build block and use it in the callback
and finally block at src/server/responses/core.ts lines 2804-2820, preserving
the existing request behavior while allowing TypeScript to narrow the values.
---
Outside diff comments:
In `@docs-site/src/content/docs/ja/guides/providers.md`:
- Around line 145-151: Restore the leading pipe on the delimiter row immediately
beneath the API-key catalog table header so it matches the header and English
source format, using a complete `| --- | --- |` row. Also correct the same
malformed delimiter rows at the unchanged table sections around the later
provider tables to keep the page consistent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b37acbbb-8d48-420d-a78e-d0e2826fba3b
📒 Files selected for processing (22)
docs-site/src/content/docs/guides/providers.mddocs-site/src/content/docs/ja/guides/providers.mddocs-site/src/content/docs/ja/reference/adapters.mddocs-site/src/content/docs/ja/reference/configuration/providers.mddocs-site/src/content/docs/ko/guides/providers.mddocs-site/src/content/docs/ko/reference/adapters.mddocs-site/src/content/docs/ko/reference/configuration/providers.mddocs-site/src/content/docs/reference/adapters.mddocs-site/src/content/docs/reference/configuration/providers.mddocs-site/src/content/docs/ru/guides/providers.mddocs-site/src/content/docs/ru/reference/adapters.mddocs-site/src/content/docs/ru/reference/configuration/providers.mddocs-site/src/content/docs/zh-cn/guides/providers.mddocs-site/src/content/docs/zh-cn/reference/adapters.mddocs-site/src/content/docs/zh-cn/reference/configuration/providers.mdgui/src/pages/Logs.tsxsrc/config.tssrc/providers/derive.tssrc/server/responses/core.tssrc/types.tssrc/usage/log.tsstructure/04_transports-and-sidecars.md
…ent write boundary CodeRabbit nitpick: extract the duplicated retryOn429 field bounds into one strict shared schema used by providerConfigSchema, the load-time sanitizer (checks derived from the schema shape), and a new retryOn429PolicyConfigError called from providerManagementConfigError so /api/providers writes reject invalid values and unknown keys before persisting. Error text never echoes values and secret-shaped unknown field names are redacted.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/config.ts`:
- Around line 1353-1358: Update the retryOn429 normalization logic around
policyShape and cleaned so that when the original policy contains keys but no
fields survive validation, delete p.retryOn429 instead of persisting an empty
object. Preserve an intentionally empty original policy object, and keep valid
cleaned fields unchanged so retryOn429 remains opt-in.
In `@src/server/auth-cors.ts`:
- Around line 424-425: Update the retryOn429Error return path in the provider
validation flow to serialize the provider name as JSON after applying
redactSecretString(name), while preserving the existing retryOn429Error message.
Add a regression case in management-provider-validation.test.ts using a
secret-shaped provider name and invalid retryOn429 policy, asserting the
management response does not expose the raw name.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 8091a749-ef7c-4db7-bf10-85d1b3690b54
📒 Files selected for processing (3)
src/config.tssrc/server/auth-cors.tstests/management-provider-validation.test.ts
CodeRabbit finding: LogDetailDialog rendered AttemptRecoveryKind values (rate-limit-429, anthropic-oauth-429, ...) directly. Map every recovery kind to an i18n key via RECOVERY_KIND_KEYS and add translations for all seven kinds in every GUI locale.
|
@coderabbitai resume Both findings from the 528b455 round are addressed:
|
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Current head and reported commits ==='
git rev-parse --short HEAD
git show --no-patch --format='commit=%H%nsubject=%s' e5021734 73a5b957
printf '%s\n' '=== Changed files ==='
git show --format='' --name-only e5021734 73a5b957
printf '%s\n' '=== Config symbol outline ==='
ast-grep outline src/config.ts --items all --type variable,function --match 'retryOn429PolicySchema|retryOn429PolicyConfigError|providerConfigSchema|loadConfig' --view expanded || true
ast-grep outline src/server/auth-cors.ts --items all --type function --match 'providerManagementConfigError' --view expanded || true
printf '%s\n' '=== Config validation and sanitizer contexts ==='
rg -n -C 12 'retryOn429PolicySchema|retryOn429PolicyConfigError|retryOn429' \
src/config.ts src/server/auth-cors.ts tests/config-user-edits.test.ts tests/management-provider-validation.test.ts
printf '%s\n' '=== Recovery kind definition and GUI mapping ==='
rg -n -C 8 'AttemptRecoveryKind|RECOVERY_KIND_KEYS|logs\.detail\.attempt\.recovery' \
src/usage/log.ts gui/src/pages/Logs.tsx gui/src
printf '%s\n' '=== Locale files that define recovery translations ==='
rg -n -C 2 'logs\.detail\.attempt\.recovery|rate-limit-429|anthropic-oauth-429' gui/src -g '*.{json,ts,tsx}' || true
printf '%s\n' '=== Exact relevant patches ==='
git show --format= --unified=30 e5021734 -- src/config.ts src/server/auth-cors.ts tests/config-user-edits.test.ts tests/management-provider-validation.test.ts
git show --format= --unified=30 73a5b957 -- gui/src/pages/Logs.tsx gui/srcLength of output: 124450
The
The
Both findings are resolved. Normal review is resumed. ✅ Action performedReviews resumed. |
…n sanitizer/error paths
CodeRabbit round on the merge head:
- Capture initialRequest and continuationRequest in consts after their build
blocks so fetch callbacks read narrowed values (let unions kept undefined).
- Replace the seven bare transportToken bumps with invalidateSameTargetRequest()
so cache invalidation is structurally coupled to credential/adapter mutations.
- Drop a retryOn429 policy whose every supplied field was invalid instead of
persisting {} (which would opt IN to retries with defaults); keep an
intentionally empty policy.
- Redact + JSON-escape the provider name in the retryOn429 management error
path; regression tests for both behaviors.
|
@coderabbitai resume Addressed the remaining five findings from the 12:00-12:10 round in 22ac868:
Validated: typecheck, privacy scan, 132 targeted tests (retry/abort/continuation/config/management) and 255 core.ts-consumer tests all pass. |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Commit and current head ==='
git rev-parse --short HEAD
git show --no-patch --format='commit=%H%nparents=%P%nsubject=%s' 22ac8685
printf '%s\n' '=== Changed files ==='
git diff-tree --no-commit-id --name-only -r 22ac8685
printf '%s\n' '=== Focused structural outline ==='
ast-grep outline src/server/responses/core.ts --items all --type function --match 'invalidateSameTargetRequest|handleResponses' --view expanded || true
ast-grep outline src/config.ts --items all --type function,variable --match 'sanitize|retryOn429|loadConfig' --view expanded || true
ast-grep outline src/server/auth-cors.ts --items all --type function --match 'providerManagementConfigError' --view expanded || true
printf '%s\n' '=== Token invalidation and narrowed request bindings ==='
rg -n -C 8 'invalidateSameTargetRequest|transportToken\s*\+=|builtInitialRequest|builtContinuationRequest|initialRequest|continuationRequest' \
src/server/responses/core.ts
printf '%s\n' '=== Sanitizer and management redaction ==='
rg -n -C 12 'retryOn429|redactSecretString|providerManagementConfigError' \
src/config.ts src/server/auth-cors.ts
printf '%s\n' '=== Relevant regression tests ==='
rg -n -C 10 'invalid.*retryOn429|intentionally empty|intentional.*empty|sk-super-secret-9876|redact|transportToken|buildRequest' \
tests/config-user-edits.test.ts tests/management-provider-validation.test.ts tests/abort-race.test.ts \
2>/dev/null || true
printf '%s\n' '=== Exact patch ==='
git show --format= --unified=45 22ac8685 -- \
src/server/responses/core.ts \
src/config.ts \
src/server/auth-cors.ts \
tests/config-user-edits.test.ts \
tests/management-provider-validation.test.tsLength of output: 117154
The five findings are addressed.
I found no regression in the inspected changes. Normal review is resumed. ✅ Action performedReviews resumed. |
|
@coderabbitai resume Verification on 22ac868:
|
|
The reported baseline comparison supports the classification of the shard-2 failures as pre-existing environment flakes. The affected areas are WebSocket auth and upgrade status tests, active-registry WebSocket test 129, Claude Messages status handling, and OpenAI compact transport. The clean typecheck, privacy scan, GUI lint, and shard-1 result provide additional validation for ✏️ Learnings added
✅ Action performedReviews resumed. |
Restore CodeRabbit docstring coverage above the 80% threshold for the invalidateSameTargetRequest helper, the RECOVERY_KIND_KEYS map, and recoveryKindKey.
|
@coderabbitai resume 7a07a2f adds JSDoc to the new diff-touched declarations (invalidateSameTargetRequest, RECOVERY_KIND_KEYS, recoveryKindKey) to restore the docstring coverage check above the 80% threshold. Typecheck and GUI lint pass. |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Commit metadata ==='
git show --no-patch --format='commit=%H%nparents=%P%nsubject=%s' 7a07a2fe
printf '%s\n' '=== Changed files ==='
git diff-tree --no-commit-id --name-status -r 7a07a2fe
printf '%s\n' '=== Exact patch ==='
git show --format= --unified=25 7a07a2fe -- \
src/server/responses/core.ts \
gui/src/pages/Logs.tsx
printf '%s\n' '=== Affected declaration contexts ==='
rg -n -C 12 \
'invalidateSameTargetRequest|RECOVERY_KIND_KEYS|recoveryKindKey' \
src/server/responses/core.ts \
gui/src/pages/Logs.tsxLength of output: 28778
The commit changes only JSDoc comments.
The implementation and call sites are unchanged. The docstring coverage fix does not introduce a functional regression. Normal review is resumed. ✅ Action performedReviews resumed. |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/server/responses/core.ts (1)
2495-2503: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe image-tier bias discovered by the main recovery loop is lost for the terminal-guard continuation.
imageTierBiasat Line 2503 is scoped to theif (!upstreamResponse.ok)block and is bumped to1at Line 2716 when the main flow needs a smaller image tier to clear an Anthropic 413.fetchTerminalGuardContinuationdeclares its own, independentimageTierBiasat Line 2770, starting at0.Because
fetchContinuation(Line 2777) keys its cache onsameTargetParsed === nextParsed, andnextParsedis always a different object fromparsed, the very first continuation call always rebuilds the request (Line 2784) using the continuation's ownimageTierBias, not the value the main flow already proved necessary. If the same oversized images are still present, the continuation's first attempt gets a redundant 413, and only its ownshouldAttemptImageTierRetrycheck (Lines 2952-2962) rediscovers the tier reduction on a second attempt.This self-recovers (matches the shared
rateLimitPolicy/rateLimitRetriespattern already applied at Line 2495-2496 for the same producer/consumer boundary), so it costs one avoidable round trip and one avoidable 413 rather than a hard failure. Hoist the discovered bias the same way the retry budget was hoisted.🐛 Proposed fix to share the discovered image tier with the continuation
const rateLimitPolicy = rateLimitRetryPolicyFor(route.provider); let rateLimitRetries = 0; + // Shared with fetchTerminalGuardContinuation below: an image-tier reduction that let the + // main request clear a 413 must not be forgotten on the very next continuation build. + let sharedImageTierBias = 0; if (!upstreamResponse.ok) {imageRetryAttempted = true; imageTierBias = 1; + sharedImageTierBias = 1; invalidateSameTargetRequest();const fetchTerminalGuardContinuation = async function* (nextParsed: OcxParsedRequest): AsyncGenerator<AdapterEvent> { - let imageTierBias = 0; + let imageTierBias = sharedImageTierBias; let response: Response | undefined;Also applies to: 2714-2718, 2769-2789
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/responses/core.ts` around lines 2495 - 2503, Hoist imageTierBias to the same shared scope as rateLimitPolicy and rateLimitRetries, initialize it once before the main recovery loop, and remove the independent declaration inside fetchTerminalGuardContinuation. Keep the existing updates in the main recovery flow and ensure fetchContinuation reuses the discovered bias when rebuilding continuation requests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/server/responses/core.ts`:
- Around line 2495-2503: Hoist imageTierBias to the same shared scope as
rateLimitPolicy and rateLimitRetries, initialize it once before the main
recovery loop, and remove the independent declaration inside
fetchTerminalGuardContinuation. Keep the existing updates in the main recovery
flow and ensure fetchContinuation reuses the discovered bias when rebuilding
continuation requests.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: d59d1154-6426-452f-ade9-23763e003ccc
📒 Files selected for processing (12)
gui/src/i18n/de.tsgui/src/i18n/en.tsgui/src/i18n/ja.tsgui/src/i18n/ko.tsgui/src/i18n/ru.tsgui/src/i18n/zh.tsgui/src/pages/Logs.tsxsrc/config.tssrc/server/auth-cors.tssrc/server/responses/core.tstests/config-user-edits.test.tstests/management-provider-validation.test.ts
|
@coderabbitai The Docstring Coverage pre-merge check currently reports 64.86% (below the 80% threshold) but does not list the missing declarations. Please list the exact functions/declarations that need docstrings so we can fix them. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Current commit and docstring-related changed files ==='
git rev-parse --short HEAD
git diff --name-only "$(git merge-base HEAD upstream/dev 2>/dev/null || git merge-base HEAD origin/dev 2>/dev/null || true)" HEAD \
| rg '\.(ts|tsx|js|jsx|mjs|cjs)$' || true
printf '%s\n' '=== Locate coverage configuration and references ==='
fd -HI -t f . \
| rg -i 'docstring|coverage|workflow|action|biome|eslint|package\.json|readme' \
| head -300
rg -n -i -C 4 'docstring.{0,30}coverage|coverage.{0,30}docstring|jsdoc|tsdoc' \
.github package.json pnpm-workspace.yaml turbo.json . 2>/dev/null \
-g '!node_modules/**' -g '!dist/**' -g '!build/**' \
| head -800 || true
printf '%s\n' '=== Declarations in diff-touched TypeScript and TSX files ==='
base="$(git merge-base HEAD upstream/dev 2>/dev/null || git merge-base HEAD origin/dev 2>/dev/null)"
git diff --name-only "$base" HEAD | rg '\.(ts|tsx)$' | while IFS= read -r file; do
printf '\n--- %s ---\n' "$file"
ast-grep outline "$file" --items all --type function,class,interface,type,variable,method --view expanded 2>/dev/null || true
doneLength of output: 9617 🏁 Script executed: #!/bin/bash
set -euo pipefail
repo="lidge-jun/opencodex"
pr="865"
printf '%s\n' '=== PR revisions and changed files ==='
gh pr view "$pr" --repo "$repo" --json baseRefOid,headRefOid,baseRefName,headRefName,commits,files \
--jq '{baseRefName, headRefName, baseRefOid, headRefOid, commits: [.commits[] | {oid: .oid, messageHeadline: .messageHeadline}], files: [.files[].path]}'
printf '%s\n' '=== PR checks ==='
gh pr checks "$pr" --repo "$repo" --json name,state,link,workflow,bucket 2>/dev/null || true
printf '%s\n' '=== Relevant package scripts ==='
node -e '
const p = require("./package.json");
for (const [name, cmd] of Object.entries(p.scripts ?? {})) {
if (/doc|string|cover|check|lint/i.test(name + " " + cmd)) console.log(`${name}: ${cmd}`);
}
'
printf '%s\n' '=== Workflow command references ==='
rg -n -i -C 5 'docstring|jsdoc|tsdoc|coverage' .github/workflows package.json gui/package.json \
-g '*.yml' -g '*.yaml' -g 'package.json' || true
printf '%s\n' '=== Candidate checker/configuration files ==='
fd -HI -t f . \
-E node_modules -E dist -E build \
| rg -i 'doc|string|cover|quality|check|audit' \
| head -500Length of output: 34718 ⚙️ CodeRabbit configuration inspected Length of output: 7440 🏁 Script executed: #!/bin/bash
set -euo pipefail
base="6a7351b4d9f729fcc33241cbb385a01a70b21e0e"
head="7a07a2fe8e1a53c9767968cae20470ffea574624"
printf '%s\n' '=== Base availability and TypeScript files changed by this PR ==='
git cat-file -e "${base}^{commit}"
git diff --name-only "$base" "$head" -- \
'src/**/*.ts' 'gui/src/**/*.ts' 'gui/src/**/*.tsx' \
| sort
printf '%s\n' '=== Added and modified declaration hunks ==='
git diff --unified=0 "$base" "$head" -- \
'src/**/*.ts' 'gui/src/**/*.ts' 'gui/src/**/*.tsx' \
| rg -n -C 2 '^\+.*\b(export\s+)?(async\s+)?function\b|^\+.*\b(class|interface|type)\b|^\+.*\b(const|let)\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=|^\+.*\b[A-Za-z_$][A-Za-z0-9_$]*\s*\([^;{}]*\)\s*\{' \
|| true
printf '%s\n' '=== Current outlines for changed production and GUI files ==='
git diff --name-only "$base" "$head" -- \
'src/**/*.ts' 'gui/src/**/*.ts' 'gui/src/**/*.tsx' \
| while IFS= read -r file; do
printf '\n--- %s ---\n' "$file"
ast-grep outline "$file" --items all --view expanded 2>/dev/null || true
done
printf '%s\n' '=== Diff sections with immediate source context ==='
git diff --unified=8 "$base" "$head" -- \
src/config.ts src/images/loop.ts src/lib/upstream-retry.ts \
src/providers/derive.ts src/providers/key-failover.ts src/providers/xai-transport.ts \
src/server/auth-cors.ts src/server/responses/core.ts src/types.ts src/usage/log.ts \
src/web-search/loop.ts gui/src/pages/Logs.tsx \
| sed -n '1,2600p'Length of output: 165012 🏁 Script executed: #!/bin/bash
set -euo pipefail
base="6a7351b4d9f729fcc33241cbb385a01a70b21e0e"
head="7a07a2fe8e1a53c9767968cae20470ffea574624"
python3 - <<'PY'
import re
import subprocess
from pathlib import Path
base = "6a7351b4d9f729fcc33241cbb385a01a70b21e0e"
head = "7a07a2fe8e1a53c9767968cae20470ffea574624"
files = subprocess.check_output(
["git", "diff", "--name-only", base, head],
text=True,
).splitlines()
files = [f for f in files if re.search(r"\.(?:ts|tsx)$", f)]
# Obtain exact added-line ranges from zero-context hunks.
ranges = {}
for f in files:
diff = subprocess.check_output(
["git", "diff", "--unified=0", base, head, "--", f],
text=True,
)
for m in re.finditer(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@", diff, re.M):
start = int(m.group(1))
count = int(m.group(2) or "1")
if count:
ranges.setdefault(f, []).append((start, start + count - 1))
# Match declaration lines that are added directly in this PR.
patterns = [
re.compile(r"^\s*(?:export\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\b"),
re.compile(r"^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?(class|interface|type|enum|namespace)\s+([A-Za-z_$][\w$]*)\b"),
re.compile(r"^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?="),
re.compile(r"^\s*(?:export\s+)?(?:async\s+)?function\*\s+([A-Za-z_$][\w$]*)\b"),
]
def is_added(f, line):
return any(a <= line <= b for a, b in ranges.get(f, []))
def attached_doc(lines, n):
# Skip blank lines directly before the declaration. A JSDoc block must then end on the
# prior non-blank line and use /** rather than ordinary // comments.
i = n - 2
while i >= 0 and not lines[i].strip():
i -= 1
if i < 0 or not lines[i].rstrip().endswith("*/"):
return False, ""
end = i
while i >= 0 and "/**" not in lines[i]:
i -= 1
if i < 0:
return False, ""
return True, " ".join(x.strip(" \t/*") for x in lines[i:end + 1]).strip()
print("=== Added declaration candidates and attached JSDoc status ===")
count = 0
for f in files:
path = Path(f)
if not path.exists():
continue
lines = path.read_text(encoding="utf-8").splitlines()
for idx, line in enumerate(lines, 1):
if not is_added(f, idx):
continue
name = None
for pat in patterns:
m = pat.match(line)
if m:
name = m.group(m.lastindex)
break
if not name:
continue
has_doc, doc = attached_doc(lines, idx)
count += 1
print(f"{f}:Line {idx}: {name}: {'JSDOC' if has_doc else 'MISSING'}")
if doc:
print(f" {doc[:300]}")
print(f"=== Added declaration candidates: {count} ===")
print("\n=== Existing exported declarations whose bodies or members changed ===")
# For each changed file, show current exported symbols with line ranges. This captures
# declarations changed internally, not only headers added by the PR.
for f in files:
print(f"\n--- {f} ---")
subprocess.run(
["ast-grep", "outline", f, "--items", "all", "--view", "expanded"],
check=False,
)
PY
printf '%s\n' '=== Direct source context for likely undocumented diff declarations ==='
rg -n -C 5 \
'^(export )?(const (en|de|ja|ko|ru|zh|DEFAULT_RATE_LIMIT_RETRY)|type AttemptRecoveryKind|interface (ImageBridgeDeps|WebSearchLoopDeps)|function providerManagementConfigError)' \
gui/src/i18n/en.ts gui/src/i18n/de.ts gui/src/i18n/ja.ts gui/src/i18n/ko.ts gui/src/i18n/ru.ts gui/src/i18n/zh.ts \
gui/src/pages/Logs.tsx src/images/loop.ts src/web-search/loop.ts src/providers/key-failover.ts src/server/auth-cors.ts src/usage/log.tsLength of output: 111601
These 13 declarations do not have an attached
The locale files already have For example: /** English i18n catalog. This object defines the compile-checked `TKey` set. */
export const en = {Add concise JSDoc to the remaining declarations. This should document all 37 assessed declarations and raise the reconstructed coverage to 100%. 🐇 You are interacting with an AI system. |
CodeRabbit docstring-coverage round: add JSDoc to the six locale catalogs, both AttemptRecoveryKind types, providerConfigSchema, ImageBridgeDeps, WebSearchLoopDeps, DEFAULT_RATE_LIMIT_RETRY, and providerManagementConfigError, bringing assessed coverage to 37/37.
|
@coderabbitai resume 2bd814d adds the missing JSDoc blocks for all 13 declarations you listed (six locale catalogs, both AttemptRecoveryKind types, providerConfigSchema, ImageBridgeDeps, WebSearchLoopDeps, DEFAULT_RATE_LIMIT_RETRY, providerManagementConfigError) — assessed coverage should now be 37/37. Typecheck and GUI lint pass. |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
gui/src/i18n/ko.ts (1)
577-579: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse Korean wording for request-rate limits.
요금 제한describes a fee or charge restriction. These labels describe HTTP request throttling. Use wording consistent with the existingpws.healthLabel.rateLimitedtranslation.Proposed wording
- "logs.detail.attempt.recovery.key429": "키 요금 제한 (429)", - "logs.detail.attempt.recovery.rateLimit429": "요금 제한 (429)", - "logs.detail.attempt.recovery.anthropicOauth429": "Anthropic OAuth 요금 제한 (429)", + "logs.detail.attempt.recovery.key429": "키 요청 한도 초과 (429)", + "logs.detail.attempt.recovery.rateLimit429": "요청 한도 초과 (429)", + "logs.detail.attempt.recovery.anthropicOauth429": "Anthropic OAuth 요청 한도 초과 (429)",🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@gui/src/i18n/ko.ts` around lines 577 - 579, Update the three recovery labels under logs.detail.attempt.recovery.key429, rateLimit429, and anthropicOauth429 to use the Korean request-throttling wording established by pws.healthLabel.rateLimited instead of 요금 제한, while preserving the existing 429 context and Anthropic OAuth naming.gui/src/pages/Logs.tsx (1)
317-323: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winAdd a localized fallback for unknown recovery kinds.
validCachedLogschecks only top-level fields. A stale or malformed cached row can contain a recovery kind that is not inRECOVERY_KIND_KEYS. The lookup then returnsundefined, andt(recoveryKindKey(kind))can break the log detail dialog or render an invalid label.Add a localized fallback or validate nested cached recovery kinds before this lookup. Add the fallback key to every locale catalog.
As per path instructions, GUI user-visible strings must go through i18n locale files.
Proposed fix
function recoveryKindKey(kind: AttemptRecoveryKind) { - return RECOVERY_KIND_KEYS[kind]; + return RECOVERY_KIND_KEYS[kind] ?? "logs.detail.attempt.recovery.unknown"; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@gui/src/pages/Logs.tsx` around lines 317 - 323, Update recoveryKindKey to return a valid localized fallback key when kind is absent from RECOVERY_KIND_KEYS, and add that fallback translation key to every locale catalog. Ensure t(recoveryKindKey(kind)) always receives a defined key without changing valid recovery-kind mappings.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@gui/src/i18n/ko.ts`:
- Around line 577-579: Update the three recovery labels under
logs.detail.attempt.recovery.key429, rateLimit429, and anthropicOauth429 to use
the Korean request-throttling wording established by pws.healthLabel.rateLimited
instead of 요금 제한, while preserving the existing 429 context and Anthropic OAuth
naming.
In `@gui/src/pages/Logs.tsx`:
- Around line 317-323: Update recoveryKindKey to return a valid localized
fallback key when kind is absent from RECOVERY_KIND_KEYS, and add that fallback
translation key to every locale catalog. Ensure t(recoveryKindKey(kind)) always
receives a defined key without changing valid recovery-kind mappings.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: c1b83b64-2d6a-4b5f-8de9-c25fcf014b51
📒 Files selected for processing (13)
gui/src/i18n/de.tsgui/src/i18n/en.tsgui/src/i18n/ja.tsgui/src/i18n/ko.tsgui/src/i18n/ru.tsgui/src/i18n/zh.tsgui/src/pages/Logs.tsxsrc/config.tssrc/images/loop.tssrc/providers/key-failover.tssrc/server/auth-cors.tssrc/usage/log.tssrc/web-search/loop.ts
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Commit and current head ==='
git rev-parse --short HEAD
git show --no-patch --format='commit=%H%nsubject=%s' 2bd814db
git diff-tree --no-commit-id --name-status -r 2bd814db
printf '%s\n' '=== Exact patch ==='
git show --format= --unified=12 2bd814db -- \
gui/src/i18n/en.ts gui/src/i18n/de.ts gui/src/i18n/ja.ts \
gui/src/i18n/ko.ts gui/src/i18n/ru.ts gui/src/i18n/zh.ts \
gui/src/pages/Logs.tsx src/config.ts src/images/loop.ts \
src/providers/key-failover.ts src/server/auth-cors.ts src/usage/log.ts \
src/web-search/loop.ts
printf '%s\n' '=== Required declaration contexts ==='
rg -n -C 6 \
'export const (en|de|ja|ko|ru|zh)\b|type AttemptRecoveryKind\b|const providerConfigSchema\b|interface ImageBridgeDeps\b|const DEFAULT_RATE_LIMIT_RETRY\b|function providerManagementConfigError\b|interface WebSearchLoopDeps\b' \
gui/src/i18n/en.ts gui/src/i18n/de.ts gui/src/i18n/ja.ts \
gui/src/i18n/ko.ts gui/src/i18n/ru.ts gui/src/i18n/zh.ts \
gui/src/pages/Logs.tsx src/config.ts src/images/loop.ts \
src/providers/key-failover.ts src/server/auth-cors.ts src/usage/log.ts \
src/web-search/loop.tsLength of output: 24780
All 13 previously missing declarations now have directly attached JSDoc blocks:
The JSDoc blocks are adjacent to their declarations. I found no functional changes in this commit. The docstring coverage finding is addressed. Normal review is resumed. ✅ Action performedReviews resumed. |
…and docs - core.ts: hoist imageTierBias to shared scope so a 413-driven tier reduction survives into the terminal-guard continuation (avoid redundant 413 round trip); consolidate the replay-derived recovery label into one replayKind value. - gui Logs.tsx + locales: localized fallback for unknown recovery kinds (logs.detail.attempt.recovery.unknown) in every locale; use the established Korean request-throttling wording for the 429 recovery labels. - config.ts: sanitize retryOn429 in configDiagnosticsFromRaw before schema validation so an invalid hand-edit cannot push the diagnostics path to a default fallback that the config command would persist over user providers; regression test added. - docs: restore leading pipes on all malformed ja providers.md table separator rows (MD055); sync the Copilot mixed-wire paragraph into ja/ko/ru/zh-cn guides before the Cursor sections; document the runTurn exception for bridge retries in the 429 design devlog.
|
@coderabbitai resume Addressed the formal-review findings (including the outside-diff ones that could not be posted inline) in d2db429 and earlier commits:
(The earlier Critical heartbeat finding was already addressed via sleepWithHeartbeats in all three live generators, and the sanitizer delete-on-empty finding via 22ac868.) Validated: typecheck, privacy scan, GUI lint, 102 targeted server tests, and GUI i18n tests all pass. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs-site/src/content/docs/ja/guides/providers.md`:
- Around line 272-275: Update the modelAdapters reference link in
docs-site/src/content/docs/ja/guides/providers.md lines 272-275 to
/ja/reference/configuration/providers/,
docs-site/src/content/docs/ko/guides/providers.md lines 273-276 to
/ko/reference/configuration/providers/,
docs-site/src/content/docs/ru/guides/providers.md lines 285-288 to
/ru/reference/configuration/providers/, and
docs-site/src/content/docs/zh-cn/guides/providers.md lines 252-255 to
/zh-cn/reference/configuration/providers/, keeping each translated guide within
its locale tree and synchronized with the English source and locale navigation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 6a8e7726-567e-45d1-9c42-a73ec62b1617
📒 Files selected for processing (15)
devlog/_plan/260802_429_same_target_retry/010_design.mddocs-site/src/content/docs/ja/guides/providers.mddocs-site/src/content/docs/ko/guides/providers.mddocs-site/src/content/docs/ru/guides/providers.mddocs-site/src/content/docs/zh-cn/guides/providers.mdgui/src/i18n/de.tsgui/src/i18n/en.tsgui/src/i18n/ja.tsgui/src/i18n/ko.tsgui/src/i18n/ru.tsgui/src/i18n/zh.tsgui/src/pages/Logs.tsxsrc/config.tssrc/server/responses/core.tstests/config-user-edits.test.ts
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Commit and changed files ==='
git rev-parse --short HEAD
git show --no-patch --format='commit=%H%nparents=%P%nsubject=%s' d2db4295
git diff-tree --no-commit-id --name-status -r d2db4295
printf '%s\n' '=== Focused changed-file outlines ==='
for file in \
src/server/responses/core.ts \
src/config.ts \
gui/src/pages/Logs.tsx \
gui/src/i18n/ko.ts \
docs-site/src/content/docs/ja/reference/configuration/providers.md \
docs-site/src/content/docs/ja/guides/providers.md \
docs-site/src/content/docs/ko/guides/providers.md \
docs-site/src/content/docs/ru/guides/providers.md \
docs-site/src/content/docs/zh-cn/guides/providers.md \
devlog/_plan/260802_429_same_target_retry/010_design.md
do
if [ -f "$file" ]; then
printf '\n--- %s ---\n' "$file"
ast-grep outline "$file" --items all --view expanded 2>/dev/null || true
fi
done
printf '%s\n' '=== Exact relevant patch ==='
git show --format= --unified=25 d2db4295 -- \
src/server/responses/core.ts src/config.ts gui/src/pages/Logs.tsx gui/src/i18n/ko.ts \
docs-site/src/content/docs/ja/reference/configuration/providers.md \
docs-site/src/content/docs/ja/guides/providers.md \
docs-site/src/content/docs/ko/guides/providers.md \
docs-site/src/content/docs/ru/guides/providers.md \
docs-site/src/content/docs/zh-cn/guides/providers.md \
devlog/_plan/260802_429_same_target_retry/010_design.md \
tests
printf '%s\n' '=== Implementation and test references ==='
rg -n -C 8 \
'imageTierBias|fetchContinuation|replayKind|recoveryKindKey|recovery\.unknown|sanitizeRetryOn429ForLoad|configDiagnosticsFromRaw|runTurn|Cursor|^\|?[\s:-]+\|' \
src/server/responses/core.ts src/config.ts gui/src/pages/Logs.tsx gui/src/i18n \
tests devlog/_plan/260802_429_same_target_retry/010_design.md \
docs-site/src/content/docs/ja docs-site/src/content/docs/ko \
docs-site/src/content/docs/ru docs-site/src/content/docs/zh-cn \
2>/dev/null || trueLength of output: 200583 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== Commit metadata and exact changed paths ==='
git show --no-patch --format='commit=%H%nsubject=%s' d2db4295
git diff-tree --no-commit-id --name-only -r d2db4295
printf '%s\n' '=== d2db4295 patch only ==='
git show --format= --unified=18 d2db4295
printf '%s\n' '=== Terminal continuation state and replay label ==='
rg -n -C 12 \
'\bimageTierBias\b|\brateLimitPolicy\b|\brateLimitRetries\b|\breplayKind\b|function fetchContinuation|const fetchContinuation' \
src/server/responses/core.ts
printf '%s\n' '=== Retry-policy diagnostic sanitization ==='
rg -n -C 12 \
'function sanitizeRetryOn429ForLoad|const sanitizeRetryOn429ForLoad|function configDiagnosticsFromRaw|const configDiagnosticsFromRaw|safeParse|providerConfigSchema' \
src/config.ts
printf '%s\n' '=== Recovery-kind fallback and locale keys ==='
rg -n -C 6 \
'RECOVERY_KIND_KEYS|function recoveryKindKey|const recoveryKindKey|recovery\.unknown|rate-limit-429|요청 한도 초과' \
gui/src/pages/Logs.tsx gui/src/i18n/en.ts gui/src/i18n/de.ts gui/src/i18n/ja.ts \
gui/src/i18n/ko.ts gui/src/i18n/ru.ts gui/src/i18n/zh.ts
printf '%s\n' '=== Documentation changes ==='
rg -n -C 5 \
'runTurn|HTTP adapters|Cursor|Copilot|mixed-wire|retry' \
devlog/_plan/260802_429_same_target_retry/010_design.md \
docs-site/src/content/docs/ja/guides/providers.md \
docs-site/src/content/docs/ko/guides/providers.md \
docs-site/src/content/docs/ru/guides/providers.md \
docs-site/src/content/docs/zh-cn/guides/providers.md \
2>/dev/null || true
printf '%s\n' '=== Markdown table delimiter rows ==='
rg -n '^\|?[-:| ]+\|[-:| ]+\|?$' \
docs-site/src/content/docs/ja/reference/configuration/providers.md \
docs-site/src/content/docs/ja/guides/providers.md \
2>/dev/null || true
printf '%s\n' '=== Relevant regression tests in d2db4295 ==='
git show --format= --name-only d2db4295 -- tests | sed '/^$/d'
git show --format= --unified=20 d2db4295 -- testsLength of output: 159886
The formal-review findings are addressed.
The inspected changes match the reported fixes. Normal review is resumed. ✅ Action performedReviews resumed. |
… tree The translated Copilot paragraphs referenced the English /reference/... path; use the locale-prefixed /ja|ko|ru|zh-cn/reference/configuration/providers/ links matching the rest of each guide.
|
@coderabbitai resume 6c7ea9f fixes the locale-link finding from the d2db429 round: the translated Copilot paragraphs now use locale-prefixed /ja|ko|ru|zh-cn/reference/configuration/providers/ links. Also: the Docstring Coverage pre-merge check still reports below 80% (it moved 64.86% -> 65.79% -> 64.10% across the last three rounds even after documenting all 13 declarations you listed). Please list the declarations it currently counts as missing on head 6c7ea9f so we can close it out. |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
repo="lidge-jun/opencodex"
pr="865"
printf '%s\n' '=== PR revision ==='
gh pr view "$pr" --repo "$repo" \
--json baseRefName,baseRefOid,headRefName,headRefOid \
--jq '{baseRefName,baseRefOid,headRefName,headRefOid}'
printf '%s\n' '=== Current checkout and reported commit ==='
git rev-parse HEAD
git show --no-patch --format='commit=%H%nparents=%P%nsubject=%s' 6c7ea9f7
printf '%s\n' '=== PR check metadata ==='
gh pr checks "$pr" --repo "$repo" --json name,state,link,workflow,bucket \
| jq '.[] | select((.name + " " + (.workflow // "")) | test("docstring"; "i"))' || true
printf '%s\n' '=== Candidate checker and workflow files ==='
fd -HI -t f . -E node_modules -E dist -E build \
| rg -i '(^|/)(docstring|jsdoc|tsdoc|.*coverage.*|.*quality.*|.*check.*)(\.[^/]+)?$|\.github/workflows/' \
| head -400
printf '%s\n' '=== Checker references ==='
rg -n -i -C 6 'docstring.{0,40}coverage|coverage.{0,40}docstring|jsdoc|tsdoc' \
.github package.json pnpm-workspace.yaml turbo.json scripts src gui 2>/dev/null \
-g '!node_modules/**' -g '!dist/**' -g '!build/**' \
| head -1200 || true
printf '%s\n' '=== Files changed from PR base ==='
base="$(gh pr view "$pr" --repo "$repo" --json baseRefOid --jq '.baseRefOid')"
printf 'base=%s\n' "$base"
git diff --name-only "$base" HEAD -- \
'*.ts' '*.tsx' '*.js' '*.jsx' '*.mjs' '*.cjs' | sort
printf '%s\n' '=== Current TypeScript/TSX declaration outline in changed source files ==='
git diff --name-only "$base" HEAD -- '*.ts' '*.tsx' \
| while IFS= read -r file; do
printf '\n--- %s ---\n' "$file"
ast-grep outline "$file" --items all --view expanded 2>/dev/null || true
doneLength of output: 87238 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== PR-quality script outline ==='
ast-grep outline .github/scripts/pr-quality.cjs --items all --view expanded
ast-grep outline .github/scripts/pr-quality.test.cjs --items all --view expanded
printf '%s\n' '=== Docstring logic and threshold references ==='
rg -n -i -C 12 'docstring|jsdoc|tsdoc|coverage|declaration|threshold|80' \
.github/scripts/pr-quality.cjs \
.github/scripts/pr-quality.test.cjs \
.github/workflows/pr-hygiene.yml \
docs-site/src/content/docs/contributing/pr-quality.md \
docs/superpowers/specs/2026-07-28-pr-quality-gates-design.md \
docs/superpowers/plans/2026-07-28-pr-quality-gates.md \
2>/dev/null
printf '%s\n' '=== PR-quality script source ==='
cat -n .github/scripts/pr-quality.cjs | sed -n '1,900p'
printf '%s\n' '=== Focused docstring tests ==='
rg -n -i -C 20 'docstring|jsdoc|tsdoc|coverage' .github/scripts/pr-quality.test.cjs \
| head -1000
printf '%s\n' '=== Workflow invocation ==='
rg -n -C 12 'pr-quality|quality' .github/workflows/pr-hygiene.yml package.json 2>/dev/nullLength of output: 46856 🏁 Script executed: #!/bin/bash
set -euo pipefail
repo="lidge-jun/opencodex"
pr="865"
head="6c7ea9f77e5fd66c24af0f1ddfccb8096b7d67df"
base="6a7351b4d9f729fcc33241cbb385a01a70b21e0e"
printf '%s\n' '=== Check runs on head ==='
gh api --paginate "repos/$repo/commits/$head/check-runs?per_page=100" \
--jq '.check_runs[] | {name,status,conclusion,details_url,app:.app.slug,output:{title:.output.title,summary:.output.summary,text:.output.text}}' \
|| true
printf '%s\n' '=== Legacy statuses on head ==='
gh api "repos/$repo/commits/$head/status" \
--jq '.statuses[] | {context,state,target_url,description,creator:.creator.login}' \
|| true
printf '%s\n' '=== PR comments that mention coverage or docstrings ==='
gh api --paginate "repos/$repo/issues/$pr/comments?per_page=100" \
--jq '.[] | select((.body // "") | test("docstring|coverage"; "i")) | {user:.user.login,created_at,html_url,body}' \
|| true
printf '%s\n' '=== Review comments that mention coverage or docstrings ==='
gh api --paginate "repos/$repo/pulls/$pr/comments?per_page=100" \
--jq '.[] | select((.body // "") | test("docstring|coverage"; "i")) | {user:.user.login,path,line,original_line,commit_id,html_url,body}' \
|| true
printf '%s\n' '=== Added or modified declaration headers in production and GUI source ==='
git diff --unified=0 "$base" "$head" -- \
'src/**/*.ts' 'gui/src/**/*.ts' 'gui/src/**/*.tsx' \
| rg -n -C 3 \
'^\+.*\b(export\s+)?(async\s+)?function(\s*\*)?\s+[A-Za-z_$][A-Za-z0-9_$]*|^\+.*\b(export\s+)?(class|interface|type|enum)\s+[A-Za-z_$][A-Za-z0-9_$]*|^\+.*\b(export\s+)?(const|let|var)\s+[A-Za-z_$][A-Za-z0-9_$]*\s*[:=]|^\+.*^\s*[A-Za-z_$][A-Za-z0-9_$]*\s*\([^;]*\)\s*\{' \
|| true
printf '%s\n' '=== Public declarations whose source range intersects a changed line ==='
python3 - <<'PY'
import json
import re
import subprocess
from pathlib import Path
base = "6a7351b4d9f729fcc33241cbb385a01a70b21e0e"
head = "6c7ea9f77e5fd66c24af0f1ddfccb8096b7d67df"
files = subprocess.check_output(
["git", "diff", "--name-only", base, head, "--", "src/**/*.ts", "gui/src/**/*.ts", "gui/src/**/*.tsx"],
text=True,
).splitlines()
def changed_ranges(file):
diff = subprocess.check_output(["git", "diff", "--unified=0", base, head, "--", file], text=True)
result = []
for m in re.finditer(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@", diff, re.M):
start, count = int(m.group(1)), int(m.group(2) or "1")
if count:
result.append((start, start + count - 1))
return result
def intersects(start, end, ranges):
return any(start <= b and a <= end for a, b in ranges)
# ast-grep JSON outline supplies locations without executing repository code.
for file in files:
try:
data = subprocess.check_output(
["ast-grep", "outline", file, "--items", "all", "--json=stream"],
text=True,
stderr=subprocess.DEVNULL,
)
except subprocess.CalledProcessError:
continue
ranges = changed_ranges(file)
print(f"\n--- {file} ---")
for raw in data.splitlines():
try:
item = json.loads(raw)
except json.JSONDecodeError:
continue
# Print raw shape once if the expected location format is absent.
text = json.dumps(item, ensure_ascii=False)
if "range" not in item:
continue
r = item["range"]
start = r["start"]["line"] + 1
end = r["end"]["line"] + 1
if intersects(start, end, ranges):
name = item.get("name") or item.get("text") or item.get("kind") or "<unnamed>"
kind = item.get("kind") or item.get("type") or ""
print(f"{start}-{end}\t{kind}\t{name}")
PYLength of output: 79956 ⚙️ CodeRabbit configuration inspected Length of output: 7440 🏁 Script executed: #!/bin/bash
set -euo pipefail
base="6a7351b4d9f729fcc33241cbb385a01a70b21e0e"
head="6c7ea9f77e5fd66c24af0f1ddfccb8096b7d67df"
printf '%s\n' '=== CodeRabbit configuration file ==='
if [ -f .coderabbit.yaml ]; then
cat -n .coderabbit.yaml
else
printf '%s\n' 'No .coderabbit.yaml in the checkout.'
fi
printf '%s\n' '=== Added TypeScript/TSX lines that declare named symbols ==='
python3 - <<'PY'
import re
import subprocess
from pathlib import Path
base = "6a7351b4d9f729fcc33241cbb385a01a70b21e0e"
head = "6c7ea9f77e5fd66c24af0f1ddfccb8096b7d67df"
files = subprocess.check_output(
["git", "diff", "--name-only", base, head, "--", "src/**/*.ts", "gui/src/**/*.ts", "gui/src/**/*.tsx"],
text=True,
).splitlines()
patterns = [
("function", re.compile(r"^\s*(?:export\s+)?(?:async\s+)?function\s*\*?\s+([A-Za-z_$][\w$]*)\b")),
("class/interface/type/enum", re.compile(r"^\s*(?:export\s+)?(?:declare\s+)?(?:abstract\s+)?(?:class|interface|type|enum)\s+([A-Za-z_$][\w$]*)\b")),
("variable", re.compile(r"^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b")),
("method", re.compile(r"^\s*(?:async\s+)?([A-Za-z_$][\w$]*)\s*\([^;{}]*\)\s*(?::\s*[^={]+)?\s*\{")),
]
def added_lines(file):
diff = subprocess.check_output(["git", "diff", "--unified=0", base, head, "--", file], text=True)
current = None
result = set()
for line in diff.splitlines():
m = re.match(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@", line)
if m:
current = int(m.group(1))
continue
if current is None:
continue
if line.startswith("+") and not line.startswith("+++"):
result.add(current)
current += 1
elif line.startswith("-") and not line.startswith("---"):
continue
else:
current += 1
return result
def jsdoc_before(lines, lineno):
i = lineno - 2
while i >= 0 and not lines[i].strip():
i -= 1
if i < 0 or not lines[i].rstrip().endswith("*/"):
return False
while i >= 0:
if "/**" in lines[i]:
return True
i -= 1
return False
candidates = []
for file in files:
lines = Path(file).read_text(encoding="utf-8").splitlines()
added = added_lines(file)
for n in sorted(added):
line = lines[n - 1]
for kind, pattern in patterns:
match = pattern.match(line)
if match:
name = match.group(1)
# Exclude control-flow constructs misread as methods.
if kind == "method" and name in {"if", "for", "while", "switch", "catch"}:
continue
candidates.append((file, n, kind, name, jsdoc_before(lines, n), line.strip()))
break
for file, line, kind, name, documented, source in candidates:
print(f"{file}:Line {line}: {kind}: {name}: {'JSDOC' if documented else 'MISSING'}")
print(f" {source}")
print(f"TOTAL={len(candidates)} DOCUMENTED={sum(x[4] for x in candidates)} MISSING={sum(not x[4] for x in candidates)}")
PY
printf '%s\n' '=== Current contexts for candidates without directly adjacent JSDoc ==='
python3 - <<'PY'
import re
import subprocess
from pathlib import Path
base = "6a7351b4d9f729fcc33241cbb385a01a70b21e0e"
head = "6c7ea9f77e5fd66c24af0f1ddfccb8096b7d67df"
files = subprocess.check_output(["git", "diff", "--name-only", base, head, "--", "src/**/*.ts", "gui/src/**/*.ts", "gui/src/**/*.tsx"], text=True).splitlines()
for file in files:
diff = subprocess.check_output(["git", "diff", "--unified=0", base, head, "--", file], text=True)
added = set()
cur = None
for row in diff.splitlines():
m = re.match(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@", row)
if m: cur = int(m.group(1)); continue
if cur is None: continue
if row.startswith("+") and not row.startswith("+++"): added.add(cur); cur += 1
elif row.startswith("-") and not row.startswith("---"): pass
else: cur += 1
lines = Path(file).read_text(encoding="utf-8").splitlines()
for n in sorted(added):
line = lines[n - 1]
if re.match(r"^\s*(?:export\s+)?(?:async\s+)?function\s*\*?\s+\w+|^\s*(?:export\s+)?(?:const|let|var)\s+\w+|^\s*(?:export\s+)?(?:class|interface|type|enum)\s+\w+", line):
i = n - 2
while i >= 0 and not lines[i].strip(): i -= 1
if i < 0 or not lines[i].rstrip().endswith("*/"):
lo, hi = max(1, n - 4), min(len(lines), n + 5)
print(f"\n--- {file}:Line {n} ---")
for j in range(lo, hi + 1):
print(f"{j}: {lines[j-1]}")
PYLength of output: 49370 🏁 Script executed: #!/bin/bash
set -euo pipefail
base="6a7351b4d9f729fcc33241cbb385a01a70b21e0e"
head="6c7ea9f77e5fd66c24af0f1ddfccb8096b7d67df"
python3 - <<'PY'
import re
import subprocess
from pathlib import Path
BASE = "6a7351b4d9f729fcc33241cbb385a01a70b21e0e"
HEAD = "6c7ea9f77e5fd66c24af0f1ddfccb8096b7d67df"
files = subprocess.check_output(
["git", "diff", "--name-only", BASE, HEAD, "--", "src/**/*.ts", "gui/src/**/*.ts", "gui/src/**/*.tsx"],
text=True,
).splitlines()
def added_lines(file):
diff = subprocess.check_output(["git", "diff", "--unified=0", BASE, HEAD, "--", file], text=True)
result, current = set(), None
for row in diff.splitlines():
m = re.match(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@", row)
if m:
current = int(m.group(1))
elif current is not None and row.startswith("+") and not row.startswith("+++"):
result.add(current); current += 1
elif current is not None and row.startswith("-") and not row.startswith("---"):
pass
elif current is not None:
current += 1
return result
def jsdoc_before(lines, line):
i = line - 2
while i >= 0 and not lines[i].strip():
i -= 1
if i < 0 or not lines[i].rstrip().endswith("*/"):
return False
while i >= 0:
if "/**" in lines[i]:
return True
i -= 1
return False
# Candidate categories: type/interface/class/enum declarations, named function declarations,
# top-level/exported values, and named callable arrow functions. Ordinary function-local state
# variables are deliberately excluded.
decl_patterns = [
("type", re.compile(r"^\s*(?:export\s+)?type\s+([A-Za-z_$][\w$]*)\b")),
("interface", re.compile(r"^\s*(?:export\s+)?interface\s+([A-Za-z_$][\w$]*)\b")),
("class", re.compile(r"^\s*(?:export\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)\b")),
("enum", re.compile(r"^\s*(?:export\s+)?(?:const\s+)?enum\s+([A-Za-z_$][\w$]*)\b")),
("function", re.compile(r"^\s*(?:export\s+)?(?:async\s+)?function\s*\*?\s+([A-Za-z_$][\w$]*)\b")),
("arrow function", re.compile(r"^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b.*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>")),
]
# Brace-depth estimate is sufficient to identify module scope here. Remove strings/comments
# conservatively so punctuation within them does not affect the depth.
def brace_delta(line):
clean = re.sub(r"//.*$", "", line)
clean = re.sub(r"/\*.*?\*/", "", clean)
clean = re.sub(r'"(?:\\.|[^"\\])*"', '""', clean)
clean = re.sub(r"'(?:\\.|[^'\\])*'", "''", clean)
return clean.count("{") - clean.count("}")
all_candidates = []
for file in files:
lines = Path(file).read_text(encoding="utf-8").splitlines()
added = added_lines(file)
depth = 0
for idx, source in enumerate(lines, 1):
module_scope = depth == 0
if idx in added:
found = None
for category, pattern in decl_patterns:
m = pattern.match(source)
if m:
found = (category, m.group(1))
break
if found is None and module_scope:
# Module-level values are part of the public/module declaration surface.
m = re.match(r"^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b", source)
if m:
found = ("module value", m.group(1))
if found:
all_candidates.append((file, idx, found[0], found[1], jsdoc_before(lines, idx), source.strip()))
depth += brace_delta(source)
print("=== Documentation-oriented added declarations ===")
for item in all_candidates:
file, line, category, name, documented, source = item
print(f"{file}:Line {line}: {category}: {name}: {'JSDOC' if documented else 'MISSING'}")
print(f" {source}")
docs = sum(item[4] for item in all_candidates)
print(f"\nTOTAL={len(all_candidates)} DOCUMENTED={docs} MISSING={len(all_candidates)-docs}")
print("\n=== Missing candidates with source context ===")
for file, line, category, name, documented, source in all_candidates:
if documented:
continue
lines = Path(file).read_text(encoding="utf-8").splitlines()
print(f"\n--- {file}:Line {line}: {category} {name} ---")
for n in range(max(1, line - 3), min(len(lines), line + 4) + 1):
print(f"{n}: {lines[n - 1]}")
PY
printf '%s\n' '=== PR declaration changes, grouped by commit ==='
git log --format='%H %s' "$base..$head" | while read -r commit subject; do
printf '\n--- %s %s ---\n' "${commit:0:8}" "$subject"
git show --format= --unified=0 "$commit" -- 'src/**/*.ts' 'gui/src/**/*.ts' 'gui/src/**/*.tsx' \
| rg '^\+.*\b(export\s+)?(async\s+)?function\s*\*?\s+[A-Za-z_$][A-Za-z0-9_$]*|^\+.*\b(export\s+)?(type|interface|class|enum)\s+[A-Za-z_$][A-Za-z0-9_$]*|^\+.*\b(export\s+)?(const|let|var)\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=' \
|| true
doneLength of output: 13606 🏁 Script executed: #!/bin/bash
set -euo pipefail
base="6a7351b4d9f729fcc33241cbb385a01a70b21e0e"
head="6c7ea9f77e5fd66c24af0f1ddfccb8096b7d67df"
python3 - <<'PY'
import re
import subprocess
from pathlib import Path
BASE = "6a7351b4d9f729fcc33241cbb385a01a70b21e0e"
HEAD = "6c7ea9f77e5fd66c24af0f1ddfccb8096b7d67df"
PATHS = ["src/*.ts", "src/**/*.ts", "gui/src/*.ts", "gui/src/*.tsx", "gui/src/**/*.ts", "gui/src/**/*.tsx"]
files = subprocess.check_output(
["git", "diff", "--name-only", BASE, HEAD, "--", *PATHS],
text=True,
).splitlines()
def added_lines(file):
diff = subprocess.check_output(["git", "diff", "--unified=0", BASE, HEAD, "--", file], text=True)
result, current = set(), None
for row in diff.splitlines():
m = re.match(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@", row)
if m:
current = int(m.group(1))
elif current is not None and row.startswith("+") and not row.startswith("+++"):
result.add(current)
current += 1
elif current is not None and row.startswith("-") and not row.startswith("---"):
pass
elif current is not None:
current += 1
return result
def jsdoc_before(lines, line):
i = line - 2
while i >= 0 and not lines[i].strip():
i -= 1
if i < 0 or not lines[i].rstrip().endswith("*/"):
return False
while i >= 0:
if "/**" in lines[i]:
return True
i -= 1
return False
patterns = [
("type", re.compile(r"^\s*(?:export\s+)?type\s+([A-Za-z_$][\w$]*)\b")),
("interface", re.compile(r"^\s*(?:export\s+)?interface\s+([A-Za-z_$][\w$]*)\b")),
("class", re.compile(r"^\s*(?:export\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)\b")),
("enum", re.compile(r"^\s*(?:export\s+)?(?:const\s+)?enum\s+([A-Za-z_$][\w$]*)\b")),
("function", re.compile(r"^\s*(?:export\s+)?(?:async\s+)?function\s*\*?\s+([A-Za-z_$][\w$]*)\b")),
("arrow function", re.compile(r"^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b.*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>")),
]
def brace_delta(line):
clean = re.sub(r"//.*$", "", line)
clean = re.sub(r"/\*.*?\*/", "", clean)
clean = re.sub(r'"(?:\\.|[^"\\])*"', '""', clean)
clean = re.sub(r"'(?:\\.|[^'\\])*'", "''", clean)
return clean.count("{") - clean.count("}")
candidates = []
for file in files:
lines = Path(file).read_text(encoding="utf-8").splitlines()
added = added_lines(file)
depth = 0
for idx, source in enumerate(lines, 1):
module_scope = depth == 0
if idx in added:
found = None
for category, pattern in patterns:
m = pattern.match(source)
if m:
found = (category, m.group(1))
break
if found is None and module_scope:
m = re.match(r"^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b", source)
if m:
found = ("module value", m.group(1))
if found:
candidates.append((file, idx, found[0], found[1], jsdoc_before(lines, idx), source.strip()))
depth += brace_delta(source)
print("=== Assessed-declaration candidate inventory ===")
for file, line, category, name, documented, source in candidates:
print(f"{file}:Line {line}: {category}: {name}: {'JSDOC' if documented else 'MISSING'}")
documented = sum(item[4] for item in candidates)
print(f"\nTOTAL={len(candidates)} DOCUMENTED={documented} MISSING={len(candidates)-documented}")
print("\n=== Missing declaration contexts ===")
for file, line, category, name, documented, source in candidates:
if documented:
continue
lines = Path(file).read_text(encoding="utf-8").splitlines()
print(f"\n--- {file}:Line {line}: {category} {name} ---")
for n in range(max(1, line - 3), min(len(lines), line + 4) + 1):
print(f"{n}: {lines[n - 1]}")
PY
printf '%s\n' '=== Complete changed source-file list ==='
git diff --name-only "$base" "$head" -- \
'src/*.ts' 'src/**/*.ts' 'gui/src/*.ts' 'gui/src/*.tsx' 'gui/src/**/*.ts' 'gui/src/**/*.tsx' \
| sortLength of output: 8944 🤖 web_search executed:
Length of output: 9183 🏁 Script executed: #!/bin/bash
set -euo pipefail
base="6a7351b4d9f729fcc33241cbb385a01a70b21e0e"
head="6c7ea9f77e5fd66c24af0f1ddfccb8096b7d67df"
printf '%s\n' '=== Directly added exported declarations ==='
git diff --unified=0 "$base" "$head" -- \
'src/*.ts' 'src/**/*.ts' 'gui/src/*.ts' 'gui/src/*.tsx' 'gui/src/**/*.ts' 'gui/src/**/*.tsx' \
| rg -n -C 2 \
'^\+\s*export\s+(?:declare\s+)?(?:async\s+)?function\s*\*?\s+[A-Za-z_$][A-Za-z0-9_$]*|^\+\s*export\s+(?:const|let|var)\s+[A-Za-z_$][A-Za-z0-9_$]*|^\+\s*export\s+(?:abstract\s+)?(?:class|interface|type|enum)\s+[A-Za-z_$][A-Za-z0-9_$]*' \
|| true
python3 - <<'PY'
import re
import subprocess
from pathlib import Path
BASE = "6a7351b4d9f729fcc33241cbb385a01a70b21e0e"
HEAD = "6c7ea9f77e5fd66c24af0f1ddfccb8096b7d67df"
PATHS = ["src/*.ts", "src/**/*.ts", "gui/src/*.ts", "gui/src/*.tsx", "gui/src/**/*.ts", "gui/src/**/*.tsx"]
files = subprocess.check_output(["git", "diff", "--name-only", BASE, HEAD, "--", *PATHS], text=True).splitlines()
direct_patterns = [
("function", re.compile(r"^\s*export\s+(?:async\s+)?function\s*\*?\s+([A-Za-z_$][\w$]*)\b")),
("value", re.compile(r"^\s*export\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b")),
("type", re.compile(r"^\s*export\s+type\s+([A-Za-z_$][\w$]*)\b")),
("interface", re.compile(r"^\s*export\s+interface\s+([A-Za-z_$][\w$]*)\b")),
("class", re.compile(r"^\s*export\s+(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)\b")),
("enum", re.compile(r"^\s*export\s+(?:const\s+)?enum\s+([A-Za-z_$][\w$]*)\b")),
]
def added_lines(file):
text = subprocess.check_output(["git", "diff", "--unified=0", BASE, HEAD, "--", file], text=True)
current, output = None, set()
for row in text.splitlines():
hunk = re.match(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@", row)
if hunk:
current = int(hunk.group(1))
elif current is not None and row.startswith("+") and not row.startswith("+++"):
output.add(current); current += 1
elif current is not None and row.startswith("-") and not row.startswith("---"):
pass
elif current is not None:
current += 1
return output
def jsdoc(lines, n):
i = n - 2
while i >= 0 and not lines[i].strip():
i -= 1
if i < 0 or not lines[i].strip().endswith("*/"):
return False
while i >= 0:
if "/**" in lines[i]:
return True
i -= 1
return False
items = []
for file in files:
lines = Path(file).read_text(encoding="utf-8").splitlines()
for n in sorted(added_lines(file)):
for category, pattern in direct_patterns:
m = pattern.match(lines[n - 1])
if m:
items.append((file, n, category, m.group(1), jsdoc(lines, n)))
break
print("=== Direct added exported declarations and JSDoc ===")
for item in items:
print(f"{item[0]}:Line {item[1]}: {item[2]} {item[3]}: {'JSDOC' if item[4] else 'MISSING'}")
print(f"TOTAL={len(items)} DOCUMENTED={sum(x[4] for x in items)} MISSING={sum(not x[4] for x in items)}")
# Get exported declarations from the current files by extracting outline text and identify
# declarations that are not direct additions but have a changed line in their textual span.
# The source context is printed for human verification.
print("\n=== Existing exported declaration headers near changed hunks ===")
for file in files:
lines = Path(file).read_text(encoding="utf-8").splitlines()
changed = added_lines(file)
exported_headers = []
for n, line in enumerate(lines, 1):
for category, pattern in direct_patterns:
m = pattern.match(line)
if m:
exported_headers.append((n, category, m.group(1)))
break
for i, (start, category, name) in enumerate(exported_headers):
end = (exported_headers[i+1][0] - 1) if i + 1 < len(exported_headers) else len(lines)
# Only report an existing header when a later changed line lies before next export.
if start not in changed and any(start <= c <= end for c in changed):
print(f"{file}:Line {start}: {category} {name}: {'JSDOC' if jsdoc(lines, start) else 'MISSING'}")
PYLength of output: 8579 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== AST JSON shape samples ==='
ast-grep run --lang ts --pattern 'export function $NAME($$$) { $$$ }' --json=stream src/config.ts | head -5 || true
ast-grep run --lang ts --pattern 'export interface $NAME { $$$ }' --json=stream src/types.ts | head -5 || true
ast-grep run --lang ts --pattern 'export type $NAME = $VALUE' --json=stream src/usage/log.ts | head -5 || true
ast-grep run --lang tsx --pattern 'export const $NAME = $VALUE' --json=stream gui/src/i18n/en.ts | head -5 || true
printf '%s\n' '=== Exact exported declaration ranges that intersect PR additions ==='
python3 - <<'PY'
import json
import re
import subprocess
from pathlib import Path
BASE = "6a7351b4d9f729fcc33241cbb385a01a70b21e0e"
HEAD = "6c7ea9f77e5fd66c24af0f1ddfccb8096b7d67df"
PATHS = ["src/*.ts", "src/**/*.ts", "gui/src/*.ts", "gui/src/*.tsx", "gui/src/**/*.ts", "gui/src/**/*.tsx"]
files = subprocess.check_output(["git", "diff", "--name-only", BASE, HEAD, "--", *PATHS], text=True).splitlines()
patterns = [
"export function $NAME($$$) { $$$ }",
"export async function $NAME($$$) { $$$ }",
"export function* $NAME($$$) { $$$ }",
"export async function* $NAME($$$) { $$$ }",
"export const $NAME = $VALUE",
"export let $NAME = $VALUE",
"export var $NAME = $VALUE",
"export interface $NAME { $$$ }",
"export type $NAME = $VALUE",
"export class $NAME { $$$ }",
"export enum $NAME { $$$ }",
]
def added_lines(file):
text = subprocess.check_output(["git", "diff", "--unified=0", BASE, HEAD, "--", file], text=True)
result, current = set(), None
for row in text.splitlines():
m = re.match(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@", row)
if m:
current = int(m.group(1))
elif current is not None and row.startswith("+") and not row.startswith("+++"):
result.add(current); current += 1
elif current is not None and row.startswith("-") and not row.startswith("---"):
pass
elif current is not None:
current += 1
return result
def jsdoc(lines, line):
i = line - 2
while i >= 0 and not lines[i].strip():
i -= 1
if i < 0 or not lines[i].rstrip().endswith("*/"):
return False
while i >= 0:
if "/**" in lines[i]:
return True
i -= 1
return False
def name_from_text(text):
m = re.search(r"export\s+(?:async\s+)?(?:function\*?\s+|(?:const|let|var|interface|type|class|enum)\s+)([A-Za-z_$][\w$]*)", text)
return m.group(1) if m else "<unknown>"
found = {}
for file in files:
lang = "tsx" if file.endswith(".tsx") else "ts"
changed = added_lines(file)
for pattern in patterns:
cmd = ["ast-grep", "run", "--lang", lang, "--pattern", pattern, "--json=stream", file]
result = subprocess.run(cmd, text=True, capture_output=True)
for row in result.stdout.splitlines():
try:
item = json.loads(row)
except json.JSONDecodeError:
continue
# ast-grep's match JSON reports range.start/end in zero-based locations.
r = item.get("range", {})
start = r.get("start", {}).get("line")
end = r.get("end", {}).get("line")
if start is None or end is None:
continue
start += 1
end += 1
if any(start <= line <= end for line in changed):
text = item.get("text", "")
key = (file, start, end, name_from_text(text))
found[key] = text
items = []
for (file, start, end, name), text in sorted(found.items()):
lines = Path(file).read_text(encoding="utf-8").splitlines()
items.append((file, start, end, name, jsdoc(lines, start)))
for file, start, end, name, documented in items:
print(f"{file}:Line {start}-{end}: {name}: {'JSDOC' if documented else 'MISSING'}")
print(f"TOTAL={len(items)} DOCUMENTED={sum(x[4] for x in items)} MISSING={sum(not x[4] for x in items)}")
print("\n=== Missing contexts ===")
for file, start, end, name, documented in items:
if documented:
continue
lines = Path(file).read_text(encoding="utf-8").splitlines()
print(f"\n--- {file}:Line {start}: {name} ---")
for line in range(max(1, start - 3), min(len(lines), start + 4) + 1):
print(f"{line}: {lines[line-1]}")
PYLength of output: 17773
The CodeRabbit pre-merge report exposes only the aggregate value, I verified the documented TypeScript eligibility rule: exported functions, values, classes, interfaces, and types require an adjacent
The current branch also has JSDoc on the changed exported interfaces and values, including My earlier list of 13 declarations was a heuristic reconstruction, not the checker’s actual candidate list. The later percentages prove that reconstruction was not reliable. I withdraw that earlier conclusion. A fresh CodeRabbit evaluation with per-declaration diagnostics is required to identify the remaining denominator precisely. The current review is rate-limited, so the existing report cannot be refreshed yet. ✅ Action performedReviews resumed. |
Summary
Adds an opt-in, provider-level
retryOn429policy: on HTTP 429 the proxy waits (upstreamRetry-Afteror a fixed interval, capped) and replays the identical pre-stream request on the same key before any multi-key failover.Why
retry_429=falsein codex-rs endpoint configs, surfaced as the misleading "exceeded retry limit" error (HTTP 429 is surfaced as "exceeded retry limit" without retrying; expose retry_429 or clarify error openai/codex#30471, still open; the suggested fix only rewords the message).Retry-Afterso clients that honor it (Claude Code) recover; Codex does not act on it.Behavior
providers.<name>.retryOn429 = { enabled?, attempts?, intervalMs?, maxIntervalMs?, respectRetryAfter? }(defaults: enabled=true, attempts=3, intervalMs=5000, maxIntervalMs=60000, respectRetryAfter=true). Default off when absent → zero behavior change.authMode: "key", or the documented omitted default for custom API-key providers). Fail closed: OAuth/forward credentials are never replayed on the same token; local runtimes (Ollama etc.) have no remote key to preserve; unknown/custom auth modes are rejected rather than guessed at.providerConfigSeednow preserves the registry auth kind (including"local") so the gate survives the seed round-trip.Retry-After.attempts)./v1/responses,/v1/chat/completions, and routed/v1/messages(all enterhandleResponses), plus the other key-auth surfaces that bypass that loop: the Responses passthrough wire (openai-responseskey-auth gateways, e.g. the built-in DeepSeek preset), the image/video bridge and web-search sidecar loops (before theiron429key rotation), and Anthropic terminal-guard continuations (before key/account failover).attemptsbudget.Retry-Afteror the fixed fallback — is capped atmaxIntervalMs; the schema capsmaxIntervalMsat 600000 (the effective per-wait cooldown ceiling). An already-expired HTTP-dateRetry-Afterretries immediately (likeRetry-After: 0).rate-limit-429recovery kind on replay sends; the image/video and web-search bridge loops restart their response-header deadline after each deliberate wait, so backoffs never consume the connect budget or surface as a 504.retryOn429fields with a warning instead of tripping the whole schema (which would hide every provider/key behind a default config); the management write boundary still rejects invalid policies.Files
src/types.ts—RateLimitRetryPolicy+OcxProviderConfig.retryOn429src/config.ts— zod validation (outer provider schema stays passthrough; a typo insideretryOn429degrades, never rejects the whole config),maxIntervalMs≤ 600000src/providers/key-failover.ts— policy normalization gated to key-auth + delay computation (Retry-After seconds/HTTP-date/0, capped)src/providers/derive.ts—providerConfigSeedpreserves the registry auth kind (incl."local")src/server/responses/core.ts— recovery-loop replay before the multi-key failoverwhile; passthrough-wire replay before the forward-pool logic; terminal-guard continuation replay before key/account failover; abort path cancels the unread 429 body firstsrc/images/loop.ts,src/web-search/loop.ts— same-target replay before theiron429key rotation (newretryOn429Policydep)src/usage/log.ts—AttemptRecoveryKindmemberrate-limit-429and its persisted-usage whitelist entrygui/src/pages/Logs.tsx— recovery-kind union includesrate-limit-429(and pre-existinganthropic-oauth-429)docs-siteconfiguration reference (all five locales),structure/04transport note,devlog/_plan/260802_429_same_target_retry/Test plan
bun test tests/rate-limit-retry.test.ts tests/server-rate-limit-retry-e2e.test.ts tests/usage-log.test.ts tests/key-failover.test.ts tests/retry-after-429.test.ts— 79 pass (policy/delay units incl. fail-closed auth gating, expired HTTP-date,Retry-After: 0, fallback cap; persistedrate-limit-429recovery kind; deterministic direct-handler abort test; e2e: replay-to-success with byte-identical bodies, opt-in passthrough, exhaustion, retry-before-failover ordering, key-authopenai-responsespassthrough replay with identical body/auth, per-request budget across a 2-key pool)tests/terminal-guard-server.test.ts); image bridge + web-search loop same-key replay before rotation withrate-limit-429telemetry (tests/images/loop.test.ts,tests/web-search.test.ts); config load degradation (tests/config-user-edits.test.ts); registry auth-kind preservation (tests/provider-registry-parity.test.ts)bun run typecheckbun run privacy:scancd gui && bun run lintdevin this environment (WS/auth/connection-refused) plus GUI tests that pass oncegui/deps are installedCommits
667ad08f— feature implementation2efb887b— audit round-1 fixes (usage-log whitelist, key-auth gating, budget scope, abort body cancel, schema cap, GUI union, docs)a06a4160— audit round-3 xhigh fixes: coverage extended to passthrough wire / image+web-search bridges / terminal continuations, fixed-fallback cap, local-mode gating, docs + tests58c36c9e— review-bot round: fail-closed auth (registry preserves local), expired Retry-After → immediate, release 429 bodies before backoff,rate-limit-429recovery on every surface, bridge header-deadline restart, continuation budget hoisted + upstream-signal sleep, config load degradation, identical-replay + budget regression tests, provider/adapter docs (5 locales)568e565c— review-bot round 2: awaited body-cancel before backoff, stale-deadline cleared pre-sleep + 499 re-check post-sleep, misnamed retryOn429 keys warn at load, opt-in + ordering wording in provider/adapter guides (5 locales)3c19337e— docstring coverage pass on the diff (JSDoc for seeded provider config, responses core helpers, image/web-search iteration prep, terminal-guard continuation, and test helpers)1af83c39— attach JSDoc directly to the remaining diff-touched declarations (terminal-guard continuation, image/web-search fetchOnce, cooldown check, provider-config interface, persisted-usage helper)d4e19788— review round 3: retry budget hoisted per request across bridge iterations, single dispatch-time attempt telemetry (recovery kind passed through), invalidenabledmaster switch discards the whole policy, secret-safe config warnings (path + type only)58bf694f— local audit round (gpt-5.6-terra + DeepSeek-V4-Flash): one request-wide 429 budget shared between the main recovery loop and the terminal-guard continuation; awaited 429-body cancellation before every backoff (all surfaces); post-sleep client-abort re-check before dispatching every replay; xAIx-grok-req-idpinned per logical request so same-key replays are byte-identical; new regression tests (shared continuation budget, continuation abort-during-wait, bridge budget not re-armed after rotation, far-future HTTP-date cap, stable xAI request id)4fee87b5— review round 4: unrecognizedretryOn429field NAMES are redacted before logging (secret-shaped property names become[REDACTED], ordinary typos stay readable)5945711b— review round 4 cleanup: rename the xAI request-id param topinnedRequestId(fallback only at transport resolution); anchor the secret-warning test assertion to the exact field+type diagnostice65106f0— review round 4 follow-up: JSON-escape the redacted field name in load warnings so control-character property names (newline/ANSI) cannot forge log lines89535fb7— review round 5: redact and JSON-escape the PROVIDER name in allretryOn429load warnings (sanitizer runs pre-validation, so the name is untrusted; secret-shaped names log as[REDACTED], control characters escaped)eb9890e5— maintainer architecture review round: (1) deliberate 429 backoffs now yield adapter heartbeats every min(10s, stall/2) in the terminal continuation and image/web-search loops, so a wait that outlives the bridge stall budget can never tripupstream_stall_timeout; (2) one immutable cached outbound request per same-target sequence — the main recovery loop, terminal continuation, and both sidecar loops reuse the exact URL/serialized body/headers, rebuilding only after key/account/oauth/tier changes (builder runs once per target, asserted); (3) regression tests: wait > stall budget still succeeds (all three surfaces), wait >connectTimeoutMsrestarts the header deadline (no 504), full-header equality on e2e replays4e172054— review round 6: clampsleepWithHeartbeatsstep to ≥1ms (non-positive interval can no longer spin unobserved); pin the contract that a policy degraded to{}still resolves as enabled (object presence = opt-in) with sanitizer + resolved-policy assertions02601815— review round 6 follow-up: normalize NaN heartbeat intervals to the 1ms step (NaN no longer aborts the wait after one beat); regression test asserts the full duration elapsese8613e36— mergeupstream/dev(docs overhaul restructure). Conflicts were doc-only (5 localeconfiguration.mdindexes): dev's restructured pages are kept, and theretryOn429reference row moved into the new per-localeconfiguration/providers.mdpages afterresponsesItemIdRepair. 302 targeted tests green on the merged tree.Draft — opened for review, not for merge.
Summary by CodeRabbit
New Features
Retry-After, configurable delays and limits, cancellation, heartbeats, and failover.retryOn429configuration for API-key authentication.Bug Fixes
Documentation